[S&R] Support data streams (#68078)

* Sort endpoint responses into indices and datastreams

The server endpoint for policies now returns data streams and
filters out backing indices from the indices array it returned
previously

* Refactor indices switch and field out of the step settings file

* Fix indices field form behaviour

* WiP on UI. Added the second table per mockup for add and edit.

* add support for creating a policy that backs up data streams end to end

* wip on restore flow - added data streams to server response

* add logic for detecting whether an index is part of a data stream

* fix public jest tests

* fix server side jest tests

* pivot to different solution in UI while we do not have data streams nicely separated

* added data stream to snapshot summary details

* move the data streams badge file closer to where it used

* add data stream badge when restoring snapshots too

* update restore copy

* fix pattern specification in indices and data streams field

* first iteration of complete policy UX

* First iteration that is ready for review

Given the contraints on working with data streams and indices in policies
at the moment the simplest implementation is to just include data streams
with indices and have the user select them there for now.

The way snapshotting behaviour is currently implemented relies entirely
on what is specified inside of "indices", this is also where data
streams must be placed. This unfortunately means that capture patterns
defined in indices will capture entire data streams too.

* delete unused import

* fix type issue in tests

* added logic for rendering out previous selection as custom pattern

* refactor indices fields to make component smaller

* added CIT for data streams badge

* Data streams > indices

* updates to relevant pieces of copy

* more copy updates

* fix types and remove unused import

* removed backing indices from restore view

* Added data stream restore warning message

* restore CITs

* first round of copy feedback

* refactor help text to provide clearer feedback, for both restore and policy forms

* Restore updates

- added spacer between title and data streams callout
- added copy to the restore settings tab to indicate that settings
  also apply to backing indices

* further copy refinements

* second round of copy feedback

* fix i18n

* added comment to mock

* line spacing fixes and created issue for tracking backing index discovery in snaphots

* refactor collapsible list logic and tests

* refactor editing managed policy check

* refactor copy to be clearer about pluralisation of data streams

* refactor file structure in components for data stream badge

* added tests for indices and data streams field helper

* refactored types and fixed i18n id per guidelines

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-07-02 15:38:24 +02:00 committed by GitHub
parent 9c76f19186
commit 0066c4b5b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1716 additions and 737 deletions

View file

@ -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 },
};

View file

@ -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 }) => (
<div>{children({ height: 500, width: 500 })}</div>
),
};
});

View file

@ -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';

View file

@ -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<RestoreSnapshotFormTestSubject>(
WithAppDependencies(RestoreSnapshot),
testBedConfig
);
const setupActions = (testBed: TestBed<RestoreSnapshotFormTestSubject>) => {
const { find } = testBed;
return {
findDataStreamCallout() {
return find('dataStreamWarningCallOut');
},
};
};
type Actions = ReturnType<typeof setupActions>;
export type RestoreSnapshotTestBed = TestBed<RestoreSnapshotFormTestSubject> & {
actions: Actions;
};
export const setup = async (): Promise<RestoreSnapshotTestBed> => {
const testBed = await initTestBed();
return {
...testBed,
actions: setupActions(testBed),
};
};
export type RestoreSnapshotFormTestSubject =
| 'snapshotRestoreStepLogistics'
| 'dataStreamWarningCallOut';

View file

@ -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) => (
<AppContextProvider value={appDependencies as any}>
<Comp {...props} />

View file

@ -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('<PolicyAdd />', () => {
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('<PolicyAdd />', () => {
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('<PolicyAdd />', () => {
// 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);
});
});

View file

@ -35,7 +35,10 @@ describe('<PolicyEdit />', () => {
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 }],
});

View file

@ -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('<RestoreSnapshot />', () => {
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);
});
});
});

View file

@ -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';

View file

@ -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');
};

View file

@ -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: [

View file

@ -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,

View file

@ -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(',')
: [];
};

View file

@ -8,3 +8,4 @@ export * from './repository';
export * from './snapshot';
export * from './restore';
export * from './policy';
export * from './indices';

View file

@ -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[];
}

View file

@ -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' */

View file

@ -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<Props> = ({ indices }) => {
const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState<boolean>(false);
const displayIndices = indices
? typeof indices === 'string'
? indices.split(',')
: indices
: undefined;
const hiddenIndicesCount =
displayIndices && displayIndices.length > 10 ? displayIndices.length - 10 : 0;
return (
<>
{displayIndices ? (
<>
<EuiText>
<ul>
{(isShowingFullIndicesList ? displayIndices : [...displayIndices].splice(0, 10)).map(
(index) => (
<li key={index}>
<EuiTitle size="xs">
<span>{index}</span>
</EuiTitle>
</li>
)
)}
</ul>
</EuiText>
{hiddenIndicesCount ? (
<>
<EuiSpacer size="xs" />
<EuiLink
onClick={() =>
isShowingFullIndicesList
? setIsShowingFullIndicesList(false)
: setIsShowingFullIndicesList(true)
}
>
{isShowingFullIndicesList ? (
<FormattedMessage
id="xpack.snapshotRestore.indicesList.indicesCollapseAllLink"
defaultMessage="Hide {count, plural, one {# index} other {# indices}}"
values={{ count: hiddenIndicesCount }}
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.indicesList.indicesExpandAllLink"
defaultMessage="Show {count, plural, one {# index} other {# indices}}"
values={{ count: hiddenIndicesCount }}
/>
)}{' '}
<EuiIcon type={isShowingFullIndicesList ? 'arrowUp' : 'arrowDown'} />
</EuiLink>
</>
) : null}
</>
) : (
<FormattedMessage
id="xpack.snapshotRestore.indicesList.allIndicesValue"
defaultMessage="All indices"
/>
)}
</>
);
};

View file

@ -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<Props> = ({ dataStreams }) => {
const { isShowingFullList, setIsShowingFullList, items, hiddenItemsCount } = useCollapsibleList({
items: dataStreams,
});
return items === 'all' ? (
<FormattedMessage
id="xpack.snapshotRestore.dataStreamsList.allDataStreamsValue"
defaultMessage="All data streams"
/>
) : (
<>
<EuiText>
<ul>
{items.map((dataStream) => (
<li key={dataStream}>
<EuiTitle size="xs">
<span>{dataStream}</span>
</EuiTitle>
</li>
))}
</ul>
</EuiText>
{hiddenItemsCount ? (
<>
<EuiSpacer size="xs" />
<EuiLink
onClick={() =>
isShowingFullList ? setIsShowingFullList(false) : setIsShowingFullList(true)
}
>
{isShowingFullList ? (
<FormattedMessage
id="xpack.snapshotRestore.dataStreamsList.dataStreamsCollapseAllLink"
defaultMessage="Hide {count, plural, one {# data stream} other {# data streams}}"
values={{ count: hiddenItemsCount }}
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.dataStreamsList.dataStreamsExpandAllLink"
defaultMessage="Show {count, plural, one {# data stream} other {# data streams}}"
values={{ count: hiddenItemsCount }}
/>
)}{' '}
<EuiIcon type={isShowingFullList ? 'arrowUp' : 'arrowDown'} />
</EuiLink>
</>
) : null}
</>
);
};

View file

@ -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<Props> = ({ indices }) => {
const { hiddenItemsCount, isShowingFullList, items, setIsShowingFullList } = useCollapsibleList({
items: indices,
});
return items === 'all' ? (
<FormattedMessage
id="xpack.snapshotRestore.indicesList.allIndicesValue"
defaultMessage="All indices"
/>
) : (
<>
<EuiText>
<ul>
{items.map((index) => (
<li key={index}>
<EuiTitle size="xs">
<span>{index}</span>
</EuiTitle>
</li>
))}
</ul>
</EuiText>
{hiddenItemsCount ? (
<>
<EuiSpacer size="xs" />
<EuiLink
onClick={() =>
isShowingFullList ? setIsShowingFullList(false) : setIsShowingFullList(true)
}
>
{isShowingFullList ? (
<FormattedMessage
id="xpack.snapshotRestore.indicesList.indicesCollapseAllLink"
defaultMessage="Hide {count, plural, one {# index} other {# indices}}"
values={{ count: hiddenItemsCount }}
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.indicesList.indicesExpandAllLink"
defaultMessage="Show {count, plural, one {# index} other {# indices}}"
values={{ count: hiddenItemsCount }}
/>
)}{' '}
<EuiIcon type={isShowingFullList ? 'arrowUp' : 'arrowDown'} />
</EuiLink>
</>
) : null}
</>
);
};

View file

@ -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';

View file

@ -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);
});
});

View file

@ -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<boolean>(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,
};
};

View file

@ -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 (
<EuiBadge data-test-subj="dataStreamBadge" color="primary">
{i18n.translate('xpack.snapshotRestore.policyForm.setSettings.dataStreamBadgeContent', {
defaultMessage: 'Data stream',
})}
</EuiBadge>
);
};

View file

@ -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,

View file

@ -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 = <D extends any>({
dataStreams,
indices,
}: {
dataStreams: D[];
indices: D[];
}) => {
return dataStreams.concat(indices);
};

View file

@ -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';

View file

@ -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<Props> = ({
policy: originalPolicy,
dataStreams,
indices,
currentUrl,
isEditing,
@ -71,6 +73,8 @@ export const PolicyForm: React.FunctionComponent<Props> = ({
},
});
const isEditingManagedPolicy = Boolean(isEditing && policy.isManagedPolicy);
// Policy validation state
const [validation, setValidation] = useState<PolicyValidation>({
isValid: true,
@ -132,6 +136,7 @@ export const PolicyForm: React.FunctionComponent<Props> = ({
<CurrentStepForm
policy={policy}
indices={indices}
dataStreams={dataStreams}
updatePolicy={updatePolicy}
isEditing={isEditing}
currentUrl={currentUrl}
@ -184,8 +189,8 @@ export const PolicyForm: React.FunctionComponent<Props> = ({
{currentStep === lastStep ? (
<EuiFlexItem grow={false}>
<EuiButton
fill={isEditing && policy.isManagedPolicy ? false : true}
color={isEditing && policy.isManagedPolicy ? 'warning' : 'secondary'}
fill={!isEditingManagedPolicy}
color={isEditingManagedPolicy ? 'warning' : 'secondary'}
iconType="check"
onClick={() => savePolicy()}
isLoading={isSaving}

View file

@ -10,6 +10,7 @@ import { PolicyValidation } from '../../../services/validation';
export interface StepProps {
policy: SlmPolicyPayload;
indices: string[];
dataStreams: string[];
updatePolicy: (updatedSettings: Partial<SlmPolicyPayload>, validationHelperData?: any) => void;
isEditing: boolean;
currentUrl: string;

View file

@ -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<StepProps> = ({
policy,
@ -148,8 +148,8 @@ export const PolicyStepReview: React.FunctionComponent<StepProps> = ({
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel"
defaultMessage="Indices"
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.dataStreamsAndIndicesLabel"
defaultMessage="Data streams and indices"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
@ -187,8 +187,8 @@ export const PolicyStepReview: React.FunctionComponent<StepProps> = ({
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel"
defaultMessage="Allow partial shards"
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialIndicesLabel"
defaultMessage="Allow partial indices"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>

View file

@ -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<StepProps> = ({
policy,
indices,
updatePolicy,
errors,
}) => {
const { i18n } = useServices();
const { config = {}, isManagedPolicy } = policy;
const updatePolicyConfig = (updatedFields: Partial<SlmPolicyPayload['config']>): 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>(!Boolean(config.indices));
const [indicesSelection, setIndicesSelection] = useState<SnapshotConfig['indices']>([...indices]);
const [indicesOptions, setIndicesOptions] = useState<EuiSelectableOption[]>(
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<string[]>(
typeof config.indices === 'string' ? config.indices.split(',') : []
);
const renderIndicesField = () => {
const indicesSwitch = (
<EuiSwitch
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel"
defaultMessage="All indices, including system indices"
/>
}
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 (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesTitle"
defaultMessage="Indices"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesDescription"
defaultMessage="Indices to back up."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<Fragment>
{isManagedPolicy ? (
<EuiToolTip
position="left"
content={
<p>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip"
defaultMessage="Cloud-managed policies require all indices."
/>
</p>
}
>
{indicesSwitch}
</EuiToolTip>
) : (
indicesSwitch
)}
{isAllIndices ? null : (
<Fragment>
<EuiSpacer size="m" />
<EuiFormRow
className="snapshotRestore__policyForm__stepSettings__indicesFieldWrapper"
label={
selectIndicesMode === 'list' ? (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel"
defaultMessage="Select indices"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
onClick={() => {
setSelectIndicesMode('custom');
updatePolicyConfig({ indices: indexPatterns.join(',') });
}}
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink"
defaultMessage="Use index patterns"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel"
defaultMessage="Index patterns"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
data-test-subj="selectIndicesLink"
onClick={() => {
setSelectIndicesMode('list');
updatePolicyConfig({ indices: indicesSelection });
}}
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink"
defaultMessage="Select indices"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
)
}
helpText={
selectIndicesMode === 'list' ? (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText"
defaultMessage="{count} {count, plural, one {index} other {indices}} will be backed up. {selectOrDeselectAllLink}"
values={{
count: config.indices && config.indices.length,
selectOrDeselectAllLink:
config.indices && config.indices.length > 0 ? (
<EuiLink
data-test-subj="deselectIndicesLink"
onClick={() => {
// 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([]);
}}
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink"
defaultMessage="Deselect all"
/>
</EuiLink>
) : (
<EuiLink
onClick={() => {
// 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]);
}}
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink"
defaultMessage="Select all"
/>
</EuiLink>
),
}}
/>
) : null
}
isInvalid={Boolean(errors.indices)}
error={errors.indices}
>
{selectIndicesMode === 'list' ? (
<EuiSelectable
allowExclusions={false}
options={indicesOptions}
onChange={(options) => {
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) => (
<EuiPanel paddingSize="s" hasShadow={false}>
{search}
{list}
</EuiPanel>
)}
</EuiSelectable>
) : (
<EuiComboBox
options={indices.map((index) => ({ 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(','),
});
}}
/>
)}
</EuiFormRow>
</Fragment>
)}
</Fragment>
</EuiFormRow>
</EuiDescribedFormGroup>
);
};
const renderIgnoreUnavailableField = () => (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescriptionTitle"
defaultMessage="Ignore unavailable indices"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription"
defaultMessage="Ignores indices that are unavailable when taking the snapshot. Otherwise, the entire snapshot will fail."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="ignoreUnavailableIndicesToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel"
defaultMessage="Ignore unavailable indices"
/>
}
checked={Boolean(config.ignoreUnavailable)}
onChange={(e) => {
updatePolicyConfig({
ignoreUnavailable: e.target.checked,
});
}}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
const renderPartialField = () => (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle"
defaultMessage="Allow partial indices"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.partialDescription"
defaultMessage="Allows snapshots of indices with primary shards that are unavailable. Otherwise, the entire snapshot will fail."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="partialIndicesToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch"
defaultMessage="Allow partial indices"
/>
}
checked={Boolean(config.partial)}
onChange={(e) => {
updatePolicyConfig({
partial: e.target.checked,
});
}}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
const renderIncludeGlobalStateField = () => (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle"
defaultMessage="Include global state"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription"
defaultMessage="Stores the global state of the cluster as part of the snapshot."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="globalStateToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel"
defaultMessage="Include global state"
/>
}
checked={config.includeGlobalState === undefined || config.includeGlobalState}
onChange={(e) => {
updatePolicyConfig({
includeGlobalState: e.target.checked,
});
}}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
return (
<div className="snapshotRestore__policyForm__stepSettings">
{/* Step title and doc link */}
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettingsTitle"
defaultMessage="Snapshot settings"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
flush="right"
href={documentationLinksService.getSnapshotDocUrl()}
target="_blank"
iconType="help"
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel"
defaultMessage="Snapshot settings docs"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
{renderIndicesField()}
{renderIgnoreUnavailableField()}
{renderPartialField()}
{renderIncludeGlobalStateField()}
</div>
);
};

View file

@ -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';

View file

@ -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<Props> = ({
onSelectionChange,
selectedIndicesAndDataStreams,
indices,
dataStreams,
}) => {
if (selectedIndicesAndDataStreams.length === 0) {
return (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.noDataStreamsOrIndicesHelpText"
defaultMessage="Nothing will be backed up. {selectAllLink}"
values={{
selectAllLink: (
<EuiLink
onClick={() => {
onSelectionChange('all');
}}
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink"
defaultMessage="Select all"
/>
</EuiLink>
),
}}
/>
);
}
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 (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.selectDataStreamsIndicesHelpText"
defaultMessage="{indicesCount} {indicesCount, plural, one {index} other {indices}} and {dataStreamsCount} {dataStreamsCount, plural, one {data stream} other {data streams}} will be backed up. {deselectAllLink}"
values={{
dataStreamsCount,
indicesCount,
deselectAllLink: (
<EuiLink
data-test-subj="deselectIndicesLink"
onClick={() => {
onSelectionChange('none');
}}
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink"
defaultMessage="Deselect all"
/>
</EuiLink>
),
}}
/>
);
};

View file

@ -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');
});
});
});

View file

@ -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<EuiSelectableOption>({
dataStreams: dataStreams.map(
(dataStream): EuiSelectableOption => {
return {
label: dataStream,
append: <DataStreamBadge />,
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';
};

View file

@ -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';

View file

@ -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<Props> = ({
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<boolean>(
!config.indices || (Array.isArray(config.indices) && config.indices.length === 0)
);
const [indicesAndDataStreamsSelection, setIndicesAndDataStreamsSelection] = useState<string[]>(
() =>
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<string[]>(() =>
typeof config.indices === 'string'
? (config.indices as string).split(',')
: Array.isArray(config.indices) && config.indices
? config.indices
: []
);
const indicesSwitch = (
<EuiSwitch
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.allDataStreamsAndIndicesLabel"
defaultMessage="All data streams and indices, including system indices"
/>
}
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 (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.dataStreamsAndIndicesTitle"
defaultMessage="Data streams and indices"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.dataStreamsAndIndicesDescription"
defaultMessage="To back up indices and data streams, manually select them or define index patterns to dynamically match them."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<Fragment>
{isManagedPolicy ? (
<EuiToolTip
position="left"
content={
<p>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip"
defaultMessage="Cloud-managed policies require all indices."
/>
</p>
}
>
{indicesSwitch}
</EuiToolTip>
) : (
indicesSwitch
)}
{isAllIndices ? null : (
<Fragment>
<EuiSpacer size="m" />
<EuiFormRow
className="snapshotRestore__policyForm__stepSettings__indicesFieldWrapper"
label={
selectIndicesMode === 'list' ? (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel"
defaultMessage="Select indices and data streams"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
onClick={() => {
setSelectIndicesMode('custom');
onUpdate({ indices: indexPatterns.join(',') });
}}
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink"
defaultMessage="Use index patterns"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel"
defaultMessage="Index patterns"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
data-test-subj="selectIndicesLink"
onClick={() => {
setSelectIndicesMode('list');
onUpdate({ indices: indicesAndDataStreamsSelection });
}}
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.dataStreamsAndIndicesToggleListLink"
defaultMessage="Select data streams and indices"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
)
}
helpText={
selectIndicesMode === 'list' ? (
<DataStreamsAndIndicesListHelpText
onSelectionChange={(selection) => {
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' ? (
<EuiSelectable
allowExclusions={false}
data-test-subj="indicesAndDataStreamsList"
options={indicesAndDataStreamsOptions}
onChange={(options) => {
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) => (
<EuiPanel paddingSize="s" hasShadow={false}>
{search}
{list}
</EuiPanel>
)}
</EuiSelectable>
) : (
<EuiComboBox
options={orderDataStreamsAndIndices({
indices: indices.map((index) => ({
label: index,
value: { isDataStream: false },
})),
dataStreams: dataStreams.map((dataStream) => ({
label: dataStream,
value: { isDataStream: true },
})),
})}
renderOption={({ label, value }) => {
if (value?.isDataStream) {
return (
<EuiFlexGroup
responsive={false}
justifyContent="spaceBetween"
alignItems="center"
>
<EuiFlexItem grow={false}>{label}</EuiFlexItem>
<EuiFlexItem grow={false}>
<DataStreamBadge />
</EuiFlexItem>
</EuiFlexGroup>
);
}
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(','),
});
}}
/>
)}
</EuiFormRow>
</Fragment>
)}
</Fragment>
</EuiFormRow>
</EuiDescribedFormGroup>
);
};

View file

@ -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';

View file

@ -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<StepProps> = ({
policy,
indices,
dataStreams,
updatePolicy,
errors,
}) => {
const { config = {}, isManagedPolicy } = policy;
const updatePolicyConfig = (updatedFields: Partial<SlmPolicyPayload['config']>): void => {
const newConfig = { ...config, ...updatedFields };
updatePolicy({
config: newConfig,
});
};
const renderIgnoreUnavailableField = () => (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescriptionTitle"
defaultMessage="Ignore unavailable indices"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription"
defaultMessage="Ignores indices that are unavailable when taking the snapshot. Otherwise, the entire snapshot will fail."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="ignoreUnavailableIndicesToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel"
defaultMessage="Ignore unavailable indices"
/>
}
checked={Boolean(config.ignoreUnavailable)}
onChange={(e) => {
updatePolicyConfig({
ignoreUnavailable: e.target.checked,
});
}}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
const renderPartialField = () => (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle"
defaultMessage="Allow partial indices"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.partialDescription"
defaultMessage="Allows snapshots of indices with primary shards that are unavailable. Otherwise, the entire snapshot will fail."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="partialIndicesToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch"
defaultMessage="Allow partial indices"
/>
}
checked={Boolean(config.partial)}
onChange={(e) => {
updatePolicyConfig({
partial: e.target.checked,
});
}}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
const renderIncludeGlobalStateField = () => (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle"
defaultMessage="Include global state"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription"
defaultMessage="Stores the global cluster state as part of the snapshot."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="globalStateToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel"
defaultMessage="Include global state"
/>
}
checked={config.includeGlobalState === undefined || config.includeGlobalState}
onChange={(e) => {
updatePolicyConfig({
includeGlobalState: e.target.checked,
});
}}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
return (
<div className="snapshotRestore__policyForm__stepSettings">
{/* Step title and doc link */}
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettingsTitle"
defaultMessage="Snapshot settings"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
flush="right"
href={documentationLinksService.getSnapshotDocUrl()}
target="_blank"
iconType="help"
>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel"
defaultMessage="Snapshot settings docs"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<IndicesAndDataStreamsField
isManagedPolicy={isManagedPolicy}
errors={errors}
dataStreams={dataStreams}
indices={indices}
policy={policy}
onUpdate={updatePolicyConfig}
/>
{renderIgnoreUnavailableField()}
{renderPartialField()}
{renderIncludeGlobalStateField()}
</div>
);
};

View file

@ -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';

View file

@ -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<Props> = ({
onSelectionChange,
selectedIndicesAndDataStreams,
indices,
dataStreams,
}) => {
if (selectedIndicesAndDataStreams.length === 0) {
return (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.noDataStreamsOrIndicesHelpText"
defaultMessage="Nothing will be restored. {selectAllLink}"
values={{
selectAllLink: (
<EuiLink
onClick={() => {
onSelectionChange('all');
}}
>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink"
defaultMessage="Select all"
/>
</EuiLink>
),
}}
/>
);
}
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 (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectDataStreamsAndIndicesHelpText"
defaultMessage="{indicesCount} {indicesCount, plural, one {index} other {indices}} and {dataStreamsCount} {dataStreamsCount, plural, one {data stream} other {data streams}} will be restored. {deselectAllLink}"
values={{
dataStreamsCount,
indicesCount,
deselectAllLink: (
<EuiLink
data-test-subj="deselectIndicesLink"
onClick={() => {
onSelectionChange('none');
}}
>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink"
defaultMessage="Deselect all"
/>
</EuiLink>
),
}}
/>
);
};

View file

@ -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: () => (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body"
defaultMessage="Each data stream requires a matching index template. Please ensure any restored data streams have a matching index template. You can restore index templates by restoring the global cluster state. However, this may overwrite existing templates, cluster settings, ingest pipelines, and lifecycle policies. {learnMoreLink} about restoring snapshots that contain data streams."
values={{
learnMoreLink: (
<EuiLink target="_blank" href={documentationLinksService.getSnapshotDocUrl()}>
{i18n.translate(
'xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body.learnMoreLink',
{ defaultMessage: 'Learn more' }
)}
</EuiLink>
),
}}
/>
),
},
};
interface Props {
dataStreamsCount: number;
}
export const DataStreamsGlobalStateCallOut: FunctionComponent<Props> = ({ dataStreamsCount }) => {
return (
<EuiCallOut
data-test-subj="dataStreamWarningCallOut"
title={i18nTexts.callout.title(dataStreamsCount)}
iconType="alert"
color="warning"
>
{i18nTexts.callout.body()}
</EuiCallOut>
);
};

View file

@ -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';

View file

@ -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<StepProps> = ({
snapshotDetails,
@ -34,10 +46,30 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
}) => {
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<StepProps> =
} = restoreSettings;
// States for choosing all indices, or a subset, including caching previously chosen subset list
const [isAllIndices, setIsAllIndices] = useState<boolean>(!Boolean(restoreIndices));
const [indicesOptions, setIndicesOptions] = useState<EuiSelectableOption[]>(
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>(
!Boolean(restoreIndices)
);
const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState<
EuiSelectableOption[]
>(() =>
orderDataStreamsAndIndices({
dataStreams: snapshotDataStreams.map(
(dataStream): EuiSelectableOption => ({
label: dataStream,
append: <DataStreamBadge />,
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<StepProps> =
// Caching state for togglable settings
const [cachedRestoreSettings, setCachedRestoreSettings] = useState<RestoreSettings>({
indices: [...snapshotIndices],
indices: [...snapshotIndicesAndDataStreams],
renamePattern: '',
renameReplacement: '',
});
return (
<div className="snapshotRestore__restoreForm__stepLogistics">
<div
data-test-subj="snapshotRestoreStepLogistics"
className="snapshotRestore__restoreForm__stepLogistics"
>
{/* Step title and doc link */}
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
@ -118,6 +175,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
{snapshotDataStreams.length ? (
<>
<EuiSpacer size="m" />
<DataStreamsGlobalStateCallOut dataStreamsCount={snapshotDataStreams.length} />
</>
) : undefined}
<EuiSpacer size="l" />
{/* Indices */}
@ -126,16 +191,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle"
defaultMessage="Indices"
id="xpack.snapshotRestore.restoreForm.stepLogistics.dataStreamsAndIndicesTitle"
defaultMessage="Data streams and indices"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription"
defaultMessage="Creates new indices if they dont exist. Restores existing indices
id="xpack.snapshotRestore.restoreForm.stepLogistics.dataStreamsAndIndicesDescription"
defaultMessage="Creates new data streams and indices if they dont exist. Opens existing indices, including backing indices for a data stream,
if they are closed and have the same number of shards as the snapshot index."
/>
}
@ -146,14 +211,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
<EuiSwitch
label={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel"
defaultMessage="All indices, including system indices"
id="xpack.snapshotRestore.restoreForm.stepLogistics.allDataStreamsAndIndicesLabel"
defaultMessage="All data streams and indices, including system indices"
/>
}
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<StepProps> =
}
}}
/>
{isAllIndices ? null : (
{isAllIndicesAndDataStreams ? null : (
<Fragment>
<EuiSpacer size="m" />
<EuiFormRow
@ -176,8 +241,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel"
defaultMessage="Select indices"
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectDataStreamsAndIndicesLabel"
defaultMessage="Select data streams and indices"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -210,8 +275,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
}}
>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink"
defaultMessage="Select indices"
id="xpack.snapshotRestore.restoreForm.stepLogistics.dataStreamsAndIndicesToggleListLink"
defaultMessage="Select data streams and indices"
/>
</EuiLink>
</EuiFlexItem>
@ -220,52 +285,35 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
}
helpText={
selectIndicesMode === 'list' ? (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText"
defaultMessage="{count} {count, plural, one {index} other {indices}} will be restored. {selectOrDeselectAllLink}"
values={{
count: restoreIndices && restoreIndices.length,
selectOrDeselectAllLink:
restoreIndices && restoreIndices.length > 0 ? (
<EuiLink
onClick={() => {
// 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: [],
});
}}
>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink"
defaultMessage="Deselect all"
/>
</EuiLink>
) : (
<EuiLink
onClick={() => {
// 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],
});
}}
>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink"
defaultMessage="Select all"
/>
</EuiLink>
),
<DataStreamsAndIndicesListHelpText
onSelectionChange={(selection) => {
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<StepProps> =
{selectIndicesMode === 'list' ? (
<EuiSelectable
allowExclusions={false}
options={indicesOptions}
options={indicesAndDataStreamsOptions}
onChange={(options) => {
const newSelectedIndices: string[] = [];
options.forEach(({ label, checked }) => {
@ -283,7 +331,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
newSelectedIndices.push(label);
}
});
setIndicesOptions(options);
setIndicesAndDataStreamsOptions(options);
updateRestoreSettings({ indices: [...newSelectedIndices] });
setCachedRestoreSettings({
...cachedRestoreSettings,
@ -302,7 +350,24 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
</EuiSelectable>
) : (
<EuiComboBox
options={snapshotIndices.map((index) => ({ label: index }))}
options={comboBoxOptions}
renderOption={({ value }) => {
return value?.isDataStream ? (
<EuiFlexGroup
responsive={false}
justifyContent="spaceBetween"
alignItems="center"
gutterSize="none"
>
<EuiFlexItem>{value.name}</EuiFlexItem>
<EuiFlexItem grow={false}>
<DataStreamBadge />
</EuiFlexItem>
</EuiFlexGroup>
) : (
value?.name
);
}}
placeholder={i18n.translate(
'xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder',
{
@ -336,22 +401,22 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
</EuiFormRow>
</EuiDescribedFormGroup>
{/* Rename indices */}
{/* Rename data streams and indices */}
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle"
defaultMessage="Rename indices"
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameDataStreamsAndIndicesTitle"
defaultMessage="Rename data streams and indices"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription"
defaultMessage="Renames indices on restore."
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameDataStreamsAndIndicesDescription"
defaultMessage="Renames data streams and indices on restore. Ensure that a matching index template exists for renamed data streams."
/>
}
fullWidth
@ -361,8 +426,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
<EuiSwitch
label={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel"
defaultMessage="Rename indices"
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameDataStreamsAndIndicesLabel"
defaultMessage="Rename data streams and indices"
/>
}
checked={isRenamingIndices}
@ -405,7 +470,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
>
<EuiFieldText
value={renamePattern}
placeholder="index_(.+)"
placeholder="data_(.+)"
onChange={(e) => {
setCachedRestoreSettings({
...cachedRestoreSettings,
@ -431,7 +496,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
>
<EuiFieldText
value={renameReplacement}
placeholder="restored_index_$1"
placeholder="restored_data_$1"
onChange={(e) => {
setCachedRestoreSettings({
...cachedRestoreSettings,

View file

@ -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<StepProps> = ({
restoreSettings,
@ -73,8 +73,8 @@ export const RestoreSnapshotStepReview: React.FunctionComponent<StepProps> = ({
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel"
defaultMessage="Indices"
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.dataStreamsAndIndicesLabel"
defaultMessage="Data streams and indices"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>

View file

@ -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<StepProps> = ({
restoreSettings,
updateRestoreSettings,
snapshotDetails,
errors,
}) => {
const { i18n } = useServices();
const { indexSettings, ignoreIndexSettings } = restoreSettings;
const { dataStreams } = snapshotDetails;
// State for index setting toggles
const [isUsingIndexSettings, setIsUsingIndexSettings] = useState<boolean>(Boolean(indexSettings));
@ -96,6 +99,23 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent<StepProps> = (
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
{dataStreams?.length ? (
<>
<EuiSpacer size="m" />
<EuiCallOut
iconType="help"
title={i18n.translate(
'xpack.snapshotRestore.restoreForm.stepSettings.dataStreamsCallout.title',
{ defaultMessage: 'Backing indices' }
)}
>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepSettings.dataStreamsCallout.description"
defaultMessage="These settings also apply to the backing indices of data streams."
/>
</EuiCallOut>
</>
) : undefined}
<EuiSpacer size="l" />
{/* Modify index settings */}

View file

@ -236,8 +236,8 @@ export const TabSummary: React.FunctionComponent<Props> = ({ policy }) => {
<EuiFlexItem data-test-subj="indices">
<EuiDescriptionListTitle data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.policyDetails.indicesLabel"
defaultMessage="Indices"
id="xpack.snapshotRestore.policyDetails.dataStreamsAndIndicesLabel"
defaultMessage="Data streams and indices"
/>
</EuiDescriptionListTitle>

View file

@ -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<Props> = ({ 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<Props> = ({ snapshotDetails }) => {
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem data-test-subj="dataStreams">
<EuiDescriptionListTitle data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.snapshotDetails.itemDataStreamsLabel"
defaultMessage="Data streams ({dataStreamsCount})"
values={{ dataStreamsCount: dataStreams.length }}
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
<CollapsibleDataStreamsList dataStreams={dataStreams} />
</EuiDescriptionListDescription>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem data-test-subj="startTime">
<EuiDescriptionListTitle data-test-subj="title">

View file

@ -25,13 +25,8 @@ export const PolicyAdd: React.FunctionComponent<RouteComponentProps> = ({
const [isSaving, setIsSaving] = useState<boolean>(false);
const [saveError, setSaveError] = useState<any>(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<RouteComponentProps> = ({
<PolicyForm
policy={emptyPolicy}
indices={indices}
dataStreams={dataStreams}
currentUrl={pathname}
isSaving={isSaving}
saveError={renderSaveError()}

View file

@ -55,9 +55,7 @@ export const PolicyEdit: React.FunctionComponent<RouteComponentProps<MatchParams
const {
error: errorLoadingIndices,
isLoading: isLoadingIndices,
data: { indices } = {
indices: [],
},
data: indicesData,
} = useLoadIndices();
// Load policy
@ -200,7 +198,8 @@ export const PolicyEdit: React.FunctionComponent<RouteComponentProps<MatchParams
) : null}
<PolicyForm
policy={policy}
indices={indices}
dataStreams={indicesData!.dataStreams}
indices={indicesData!.indices}
currentUrl={pathname}
isEditing={true}
isSaving={isSaving}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { API_BASE_PATH } from '../../../../common/constants';
import { SlmPolicy, SlmPolicyPayload } from '../../../../common/types';
import { SlmPolicy, SlmPolicyPayload, PolicyIndicesResponse } from '../../../../common/types';
import {
UIM_POLICY_EXECUTE,
UIM_POLICY_DELETE,
@ -40,7 +40,7 @@ export const useLoadPolicy = (name: SlmPolicy['name']) => {
};
export const useLoadIndices = () => {
return useRequest({
return useRequest<PolicyIndicesResponse>({
path: `${API_BASE_PATH}policies/indices`,
method: 'get',
});

View file

@ -18,6 +18,6 @@ export const sendRequest = (config: SendRequestConfig) => {
return _sendRequest<any, CustomError>(httpService.httpClient, config);
};
export const useRequest = (config: UseRequestConfig) => {
return _useRequest<any, CustomError>(httpService.httpClient, config);
export const useRequest = <D = any>(config: UseRequestConfig) => {
return _useRequest<D, CustomError>(httpService.httpClient, config);
};

View file

@ -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.',
})
);
}

View file

@ -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 cant 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 cant 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

View file

@ -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 });
});

View file

@ -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)) {

View file

@ -15,6 +15,7 @@ const defaultSnapshot = {
versionId: undefined,
version: undefined,
indices: [],
dataStreams: [],
includeGlobalState: undefined,
state: undefined,
startTime: undefined,

View file

@ -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'];

View file

@ -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',

View file

@ -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": "部分復元",

View file

@ -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": "部分还原",