diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
index 45b8b23cae477..605265f7311ba 100644
--- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
@@ -6,7 +6,7 @@
*/
import sinon, { SinonFakeServer } from 'sinon';
-import { API_BASE_PATH } from '../../../common/constants';
+import { API_BASE_PATH } from '../../../common';
type HttpResponse = Record | any[];
@@ -54,7 +54,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
const setLoadSnapshotsResponse = (response: HttpResponse = {}) => {
- const defaultResponse = { errors: {}, snapshots: [], repositories: [] };
+ const defaultResponse = { errors: {}, snapshots: [], repositories: [], total: 0 };
server.respondWith('GET', `${API_BASE_PATH}snapshots`, mockResponse(defaultResponse, response));
};
diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
index 52303e1134f9d..071868e23f7fe 100644
--- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
+++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
@@ -8,7 +8,7 @@
import { act } from 'react-dom/test-utils';
import * as fixtures from '../../test/fixtures';
import { SNAPSHOT_STATE } from '../../public/application/constants';
-import { API_BASE_PATH } from '../../common/constants';
+import { API_BASE_PATH } from '../../common';
import {
setupEnvironment,
pageHelpers,
@@ -431,6 +431,7 @@ describe('', () => {
httpRequestsMockHelpers.setLoadSnapshotsResponse({
snapshots: [],
repositories: ['my-repo'],
+ total: 0,
});
testBed = await setup();
@@ -469,6 +470,7 @@ describe('', () => {
httpRequestsMockHelpers.setLoadSnapshotsResponse({
snapshots,
repositories: [REPOSITORY_NAME],
+ total: 2,
});
testBed = await setup();
@@ -501,18 +503,10 @@ describe('', () => {
});
});
- test('should show a warning if the number of snapshots exceeded the limit', () => {
- // We have mocked the SNAPSHOT_LIST_MAX_SIZE to 2, so the warning should display
- const { find, exists } = testBed;
- expect(exists('maxSnapshotsWarning')).toBe(true);
- expect(find('maxSnapshotsWarning').text()).toContain(
- 'Cannot show the full list of snapshots'
- );
- });
-
test('should show a warning if one repository contains errors', async () => {
httpRequestsMockHelpers.setLoadSnapshotsResponse({
snapshots,
+ total: 2,
repositories: [REPOSITORY_NAME],
errors: {
repository_with_errors: {
diff --git a/x-pack/plugins/snapshot_restore/common/constants.ts b/x-pack/plugins/snapshot_restore/common/constants.ts
index a7c83ecf702e0..b18e118dc5ff6 100644
--- a/x-pack/plugins/snapshot_restore/common/constants.ts
+++ b/x-pack/plugins/snapshot_restore/common/constants.ts
@@ -65,9 +65,3 @@ export const TIME_UNITS: { [key: string]: 'd' | 'h' | 'm' | 's' } = {
MINUTE: 'm',
SECOND: 's',
};
-
-/**
- * [Temporary workaround] In order to prevent client-side performance issues for users with a large number of snapshots,
- * we set a hard-coded limit on the number of snapshots we return from the ES snapshots API
- */
-export const SNAPSHOT_LIST_MAX_SIZE = 1000;
diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/index.ts b/x-pack/plugins/snapshot_restore/public/application/lib/index.ts
index 1ec4d5b2907f2..19a42bef4cea4 100644
--- a/x-pack/plugins/snapshot_restore/public/application/lib/index.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/lib/index.ts
@@ -6,3 +6,12 @@
*/
export { useDecodedParams } from './use_decoded_params';
+
+export {
+ SortField,
+ SortDirection,
+ SnapshotListParams,
+ getListParams,
+ getQueryFromListParams,
+ DEFAULT_SNAPSHOT_LIST_PARAMS,
+} from './snapshot_list_params';
diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts b/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts
new file mode 100644
index 0000000000000..b75a3e01fb617
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Direction, Query } from '@elastic/eui';
+
+export type SortField =
+ | 'snapshot'
+ | 'repository'
+ | 'indices'
+ | 'startTimeInMillis'
+ | 'durationInMillis'
+ | 'shards.total'
+ | 'shards.failed';
+
+export type SortDirection = Direction;
+
+interface SnapshotTableParams {
+ sortField: SortField;
+ sortDirection: SortDirection;
+ pageIndex: number;
+ pageSize: number;
+}
+interface SnapshotSearchParams {
+ searchField?: string;
+ searchValue?: string;
+ searchMatch?: string;
+ searchOperator?: string;
+}
+export type SnapshotListParams = SnapshotTableParams & SnapshotSearchParams;
+
+// By default, we'll display the most recent snapshots at the top of the table (no query).
+export const DEFAULT_SNAPSHOT_LIST_PARAMS: SnapshotListParams = {
+ sortField: 'startTimeInMillis',
+ sortDirection: 'desc',
+ pageIndex: 0,
+ pageSize: 20,
+};
+
+const resetSearchOptions = (listParams: SnapshotListParams): SnapshotListParams => ({
+ ...listParams,
+ searchField: undefined,
+ searchValue: undefined,
+ searchMatch: undefined,
+ searchOperator: undefined,
+});
+
+// to init the query for repository and policyName search passed via url
+export const getQueryFromListParams = (listParams: SnapshotListParams): Query => {
+ const { searchField, searchValue } = listParams;
+ if (!searchField || !searchValue) {
+ return Query.MATCH_ALL;
+ }
+ return Query.parse(`${searchField}=${searchValue}`);
+};
+
+export const getListParams = (listParams: SnapshotListParams, query: Query): SnapshotListParams => {
+ if (!query) {
+ return resetSearchOptions(listParams);
+ }
+ const clause = query.ast.clauses[0];
+ if (!clause) {
+ return resetSearchOptions(listParams);
+ }
+ // term queries (free word search) converts to snapshot name search
+ if (clause.type === 'term') {
+ return {
+ ...listParams,
+ searchField: 'snapshot',
+ searchValue: String(clause.value),
+ searchMatch: clause.match,
+ searchOperator: 'eq',
+ };
+ }
+ if (clause.type === 'field') {
+ return {
+ ...listParams,
+ searchField: clause.field,
+ searchValue: String(clause.value),
+ searchMatch: clause.match,
+ searchOperator: clause.operator,
+ };
+ }
+ return resetSearchOptions(listParams);
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/index.ts b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/index.ts
similarity index 55%
rename from x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/index.ts
rename to x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/index.ts
index 7ea85f4150900..8c69ca0297e3e 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/index.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/index.ts
@@ -6,3 +6,7 @@
*/
export { SnapshotTable } from './snapshot_table';
+export { RepositoryError } from './repository_error';
+export { RepositoryEmptyPrompt } from './repository_empty_prompt';
+export { SnapshotEmptyPrompt } from './snapshot_empty_prompt';
+export { SnapshotSearchBar } from './snapshot_search_bar';
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_empty_prompt.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_empty_prompt.tsx
new file mode 100644
index 0000000000000..4c5e050ea489c
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_empty_prompt.tsx
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import { EuiButton, EuiEmptyPrompt, EuiPageContent } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { reactRouterNavigate } from '../../../../../shared_imports';
+import { linkToAddRepository } from '../../../../services/navigation';
+
+export const RepositoryEmptyPrompt: React.FunctionComponent = () => {
+ const history = useHistory();
+ return (
+
+
+
+
+ }
+ body={
+ <>
+
+
+
+
+
+
+
+
+ >
+ }
+ data-test-subj="emptyPrompt"
+ />
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_error.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_error.tsx
new file mode 100644
index 0000000000000..d3902770333cc
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_error.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiEmptyPrompt, EuiLink, EuiPageContent } from '@elastic/eui';
+import { reactRouterNavigate } from '../../../../../shared_imports';
+import { linkToRepositories } from '../../../../services/navigation';
+
+export const RepositoryError: React.FunctionComponent = () => {
+ const history = useHistory();
+ return (
+
+
+
+
+ }
+ body={
+
+
+
+
+ ),
+ }}
+ />
+
+ }
+ />
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_empty_prompt.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_empty_prompt.tsx
new file mode 100644
index 0000000000000..2cfc1d5ebefc5
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_empty_prompt.tsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { Fragment } from 'react';
+import { useHistory } from 'react-router-dom';
+import { EuiButton, EuiEmptyPrompt, EuiIcon, EuiLink, EuiPageContent } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../../common';
+import { reactRouterNavigate, WithPrivileges } from '../../../../../shared_imports';
+import { linkToAddPolicy, linkToPolicies } from '../../../../services/navigation';
+import { useCore } from '../../../../app_context';
+
+export const SnapshotEmptyPrompt: React.FunctionComponent<{ policiesCount: number }> = ({
+ policiesCount,
+}) => {
+ const { docLinks } = useCore();
+ const history = useHistory();
+ return (
+
+
+
+
+ }
+ body={
+ `cluster.${name}`)}>
+ {({ hasPrivileges }) =>
+ hasPrivileges ? (
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+ {policiesCount === 0 ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ ) : (
+
+
+
+
+
+
+ {' '}
+
+
+
+
+ )
+ }
+
+ }
+ data-test-subj="emptyPrompt"
+ />
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_search_bar.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_search_bar.tsx
new file mode 100644
index 0000000000000..b3e2c24e396f0
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_search_bar.tsx
@@ -0,0 +1,178 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import useDebounce from 'react-use/lib/useDebounce';
+
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { SearchFilterConfig } from '@elastic/eui/src/components/search_bar/search_filters';
+import { SchemaType } from '@elastic/eui/src/components/search_bar/search_box';
+import { EuiSearchBarOnChangeArgs } from '@elastic/eui/src/components/search_bar/search_bar';
+import { EuiButton, EuiCallOut, EuiSearchBar, EuiSpacer, Query } from '@elastic/eui';
+import { SnapshotDeleteProvider } from '../../../../components';
+import { SnapshotDetails } from '../../../../../../common/types';
+import { getQueryFromListParams, SnapshotListParams, getListParams } from '../../../../lib';
+
+const SEARCH_DEBOUNCE_VALUE_MS = 200;
+
+const onlyOneClauseMessage = i18n.translate(
+ 'xpack.snapshotRestore.snapshotList.searchBar.onlyOneClauseMessage',
+ {
+ defaultMessage: 'You can only use one clause in the search bar',
+ }
+);
+// for now limit the search bar to snapshot, repository and policyName queries
+const searchSchema: SchemaType = {
+ strict: true,
+ fields: {
+ snapshot: {
+ type: 'string',
+ },
+ repository: {
+ type: 'string',
+ },
+ policyName: {
+ type: 'string',
+ },
+ },
+};
+
+interface Props {
+ listParams: SnapshotListParams;
+ setListParams: (listParams: SnapshotListParams) => void;
+ reload: () => void;
+ selectedItems: SnapshotDetails[];
+ onSnapshotDeleted: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void;
+ repositories: string[];
+}
+
+export const SnapshotSearchBar: React.FunctionComponent = ({
+ listParams,
+ setListParams,
+ reload,
+ selectedItems,
+ onSnapshotDeleted,
+ repositories,
+}) => {
+ const [cachedListParams, setCachedListParams] = useState(listParams);
+ // send the request after the user has stopped typing
+ useDebounce(
+ () => {
+ setListParams(cachedListParams);
+ },
+ SEARCH_DEBOUNCE_VALUE_MS,
+ [cachedListParams]
+ );
+
+ const deleteButton = selectedItems.length ? (
+
+ {(
+ deleteSnapshotPrompt: (
+ ids: Array<{ snapshot: string; repository: string }>,
+ onSuccess?: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void
+ ) => void
+ ) => {
+ return (
+
+ deleteSnapshotPrompt(
+ selectedItems.map(({ snapshot, repository }) => ({ snapshot, repository })),
+ onSnapshotDeleted
+ )
+ }
+ color="danger"
+ data-test-subj="srSnapshotListBulkDeleteActionButton"
+ >
+
+
+ );
+ }}
+
+ ) : (
+ []
+ );
+ const searchFilters: SearchFilterConfig[] = [
+ {
+ type: 'field_value_selection' as const,
+ field: 'repository',
+ name: i18n.translate('xpack.snapshotRestore.snapshotList.table.repositoryFilterLabel', {
+ defaultMessage: 'Repository',
+ }),
+ operator: 'exact',
+ multiSelect: false,
+ options: repositories.map((repository) => ({
+ value: repository,
+ view: repository,
+ })),
+ },
+ ];
+ const reloadButton = (
+
+
+
+ );
+
+ const [query, setQuery] = useState(getQueryFromListParams(listParams));
+ const [error, setError] = useState(null);
+
+ const onSearchBarChange = (args: EuiSearchBarOnChangeArgs) => {
+ const { query: changedQuery, error: queryError } = args;
+ if (queryError) {
+ setError(queryError);
+ } else if (changedQuery) {
+ setError(null);
+ setQuery(changedQuery);
+ if (changedQuery.ast.clauses.length > 1) {
+ setError({ name: onlyOneClauseMessage, message: onlyOneClauseMessage });
+ } else {
+ setCachedListParams(getListParams(listParams, changedQuery));
+ }
+ }
+ };
+
+ return (
+ <>
+
+
+ {error ? (
+ <>
+
+ }
+ />
+
+ >
+ ) : null}
+ >
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx
similarity index 71%
rename from x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx
rename to x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx
index 47f8d9b833e40..5db702fcbd963 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx
@@ -7,34 +7,28 @@
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types';
+
import {
- EuiButton,
- EuiInMemoryTable,
EuiLink,
- Query,
EuiLoadingSpinner,
EuiToolTip,
EuiButtonIcon,
+ Criteria,
+ EuiBasicTable,
} from '@elastic/eui';
-
import { SnapshotDetails } from '../../../../../../common/types';
-import { UseRequestResponse } from '../../../../../shared_imports';
+import { UseRequestResponse, reactRouterNavigate } from '../../../../../shared_imports';
import { SNAPSHOT_STATE, UIM_SNAPSHOT_SHOW_DETAILS_CLICK } from '../../../../constants';
import { useServices } from '../../../../app_context';
-import { linkToRepository, linkToRestoreSnapshot } from '../../../../services/navigation';
+import {
+ linkToRepository,
+ linkToRestoreSnapshot,
+ linkToSnapshot as openSnapshotDetailsUrl,
+} from '../../../../services/navigation';
+import { SnapshotListParams, SortDirection, SortField } from '../../../../lib';
import { DataPlaceholder, FormattedDateTime, SnapshotDeleteProvider } from '../../../../components';
-
-import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public';
-
-interface Props {
- snapshots: SnapshotDetails[];
- repositories: string[];
- reload: UseRequestResponse['resendRequest'];
- openSnapshotDetailsUrl: (repositoryName: string, snapshotId: string) => string;
- repositoryFilter?: string;
- policyFilter?: string;
- onSnapshotDeleted: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void;
-}
+import { SnapshotSearchBar } from './snapshot_search_bar';
const getLastSuccessfulManagedSnapshot = (
snapshots: SnapshotDetails[]
@@ -51,15 +45,28 @@ const getLastSuccessfulManagedSnapshot = (
return successfulSnapshots[0];
};
-export const SnapshotTable: React.FunctionComponent = ({
- snapshots,
- repositories,
- reload,
- openSnapshotDetailsUrl,
- onSnapshotDeleted,
- repositoryFilter,
- policyFilter,
-}) => {
+interface Props {
+ snapshots: SnapshotDetails[];
+ repositories: string[];
+ reload: UseRequestResponse['resendRequest'];
+ onSnapshotDeleted: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void;
+ listParams: SnapshotListParams;
+ setListParams: (listParams: SnapshotListParams) => void;
+ totalItemCount: number;
+ isLoading: boolean;
+}
+
+export const SnapshotTable: React.FunctionComponent = (props: Props) => {
+ const {
+ snapshots,
+ repositories,
+ reload,
+ onSnapshotDeleted,
+ listParams,
+ setListParams,
+ totalItemCount,
+ isLoading,
+ } = props;
const { i18n, uiMetricService, history } = useServices();
const [selectedItems, setSelectedItems] = useState([]);
@@ -71,7 +78,7 @@ export const SnapshotTable: React.FunctionComponent = ({
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.snapshotColumnTitle', {
defaultMessage: 'Snapshot',
}),
- truncateText: true,
+ truncateText: false,
sortable: true,
render: (snapshotId: string, snapshot: SnapshotDetails) => (
= ({
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.repositoryColumnTitle', {
defaultMessage: 'Repository',
}),
- truncateText: true,
+ truncateText: false,
sortable: true,
render: (repositoryName: string) => (
= ({
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.startTimeColumnTitle', {
defaultMessage: 'Date created',
}),
- truncateText: true,
+ truncateText: false,
sortable: true,
render: (startTimeInMillis: number) => (
@@ -263,30 +270,20 @@ export const SnapshotTable: React.FunctionComponent = ({
},
];
- // By default, we'll display the most recent snapshots at the top of the table.
- const sorting = {
+ const sorting: EuiTableSortingType = {
sort: {
- field: 'startTimeInMillis',
- direction: 'desc' as const,
+ field: listParams.sortField as keyof SnapshotDetails,
+ direction: listParams.sortDirection,
},
};
const pagination = {
- initialPageSize: 20,
+ pageIndex: listParams.pageIndex,
+ pageSize: listParams.pageSize,
+ totalItemCount,
pageSizeOptions: [10, 20, 50],
};
- const searchSchema = {
- fields: {
- repository: {
- type: 'string',
- },
- policyName: {
- type: 'string',
- },
- },
- };
-
const selection = {
onSelectionChange: (newSelectedItems: SnapshotDetails[]) => setSelectedItems(newSelectedItems),
selectable: ({ snapshot }: SnapshotDetails) =>
@@ -306,103 +303,44 @@ export const SnapshotTable: React.FunctionComponent = ({
},
};
- const search = {
- toolsLeft: selectedItems.length ? (
-
- {(
- deleteSnapshotPrompt: (
- ids: Array<{ snapshot: string; repository: string }>,
- onSuccess?: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void
- ) => void
- ) => {
- return (
-
- deleteSnapshotPrompt(
- selectedItems.map(({ snapshot, repository }) => ({ snapshot, repository })),
- onSnapshotDeleted
- )
- }
- color="danger"
- data-test-subj="srSnapshotListBulkDeleteActionButton"
- >
-
-
- );
- }}
-
- ) : undefined,
- toolsRight: (
-
-
-
- ),
- box: {
- incremental: true,
- schema: searchSchema,
- },
- filters: [
- {
- type: 'field_value_selection' as const,
- field: 'repository',
- name: i18n.translate('xpack.snapshotRestore.snapshotList.table.repositoryFilterLabel', {
- defaultMessage: 'Repository',
- }),
- multiSelect: false,
- options: repositories.map((repository) => ({
- value: repository,
- view: repository,
- })),
- },
- ],
- defaultQuery: policyFilter
- ? Query.parse(`policyName="${policyFilter}"`, {
- schema: {
- ...searchSchema,
- strict: true,
- },
- })
- : repositoryFilter
- ? Query.parse(`repository="${repositoryFilter}"`, {
- schema: {
- ...searchSchema,
- strict: true,
- },
- })
- : '',
- };
-
return (
- ({
- 'data-test-subj': 'row',
- })}
- cellProps={() => ({
- 'data-test-subj': 'cell',
- })}
- data-test-subj="snapshotTable"
- />
+ <>
+
+ ) => {
+ const { page: { index, size } = {}, sort: { field, direction } = {} } = criteria;
+
+ setListParams({
+ ...listParams,
+ sortField: (field as SortField) ?? listParams.sortField,
+ sortDirection: (direction as SortDirection) ?? listParams.sortDirection,
+ pageIndex: index ?? listParams.pageIndex,
+ pageSize: size ?? listParams.pageSize,
+ });
+ }}
+ loading={isLoading}
+ isSelectable={true}
+ selection={selection}
+ pagination={pagination}
+ rowProps={() => ({
+ 'data-test-subj': 'row',
+ })}
+ cellProps={() => ({
+ 'data-test-subj': 'cell',
+ })}
+ data-test-subj="snapshotTable"
+ />
+ >
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx
index 92c03d1be936d..da7ec42f746a3 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx
@@ -5,37 +5,26 @@
* 2.0.
*/
-import React, { Fragment, useState, useEffect } from 'react';
+import React, { useState, useEffect } from 'react';
import { parse } from 'query-string';
import { FormattedMessage } from '@kbn/i18n/react';
import { RouteComponentProps } from 'react-router-dom';
-import {
- EuiPageContent,
- EuiButton,
- EuiCallOut,
- EuiLink,
- EuiEmptyPrompt,
- EuiSpacer,
- EuiIcon,
-} from '@elastic/eui';
+import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui';
-import { APP_SLM_CLUSTER_PRIVILEGES, SNAPSHOT_LIST_MAX_SIZE } from '../../../../../common';
-import { WithPrivileges, PageLoading, PageError, Error } from '../../../../shared_imports';
+import { PageLoading, PageError, Error, reactRouterNavigate } from '../../../../shared_imports';
import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants';
import { useLoadSnapshots } from '../../../services/http';
-import {
- linkToRepositories,
- linkToAddRepository,
- linkToPolicies,
- linkToAddPolicy,
- linkToSnapshot,
-} from '../../../services/navigation';
-import { useCore, useServices } from '../../../app_context';
-import { useDecodedParams } from '../../../lib';
-import { SnapshotDetails } from './snapshot_details';
-import { SnapshotTable } from './snapshot_table';
+import { linkToRepositories } from '../../../services/navigation';
+import { useServices } from '../../../app_context';
+import { useDecodedParams, SnapshotListParams, DEFAULT_SNAPSHOT_LIST_PARAMS } from '../../../lib';
-import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public';
+import { SnapshotDetails } from './snapshot_details';
+import {
+ SnapshotTable,
+ RepositoryEmptyPrompt,
+ SnapshotEmptyPrompt,
+ RepositoryError,
+} from './components';
interface MatchParams {
repositoryName?: string;
@@ -47,22 +36,22 @@ export const SnapshotList: React.FunctionComponent {
const { repositoryName, snapshotId } = useDecodedParams();
+ const [listParams, setListParams] = useState(DEFAULT_SNAPSHOT_LIST_PARAMS);
const {
error,
+ isInitialRequest,
isLoading,
- data: { snapshots = [], repositories = [], policies = [], errors = {} },
+ data: {
+ snapshots = [],
+ repositories = [],
+ policies = [],
+ errors = {},
+ total: totalSnapshotsCount,
+ },
resendRequest: reload,
- } = useLoadSnapshots();
+ } = useLoadSnapshots(listParams);
- const { uiMetricService, i18n } = useServices();
- const { docLinks } = useCore();
-
- const openSnapshotDetailsUrl = (
- repositoryNameToOpen: string,
- snapshotIdToOpen: string
- ): string => {
- return linkToSnapshot(repositoryNameToOpen, snapshotIdToOpen);
- };
+ const { uiMetricService } = useServices();
const closeSnapshotDetails = () => {
history.push(`${BASE_PATH}/snapshots`);
@@ -86,22 +75,32 @@ export const SnapshotList: React.FunctionComponent(undefined);
- const [filteredPolicy, setFilteredPolicy] = useState(undefined);
useEffect(() => {
if (search) {
const parsedParams = parse(search.replace(/^\?/, ''), { sort: false });
const { repository, policy } = parsedParams;
- if (policy && policy !== filteredPolicy) {
- setFilteredPolicy(String(policy));
+ if (policy) {
+ setListParams((prev: SnapshotListParams) => ({
+ ...prev,
+ searchField: 'policyName',
+ searchValue: String(policy),
+ searchMatch: 'must',
+ searchOperator: 'exact',
+ }));
history.replace(`${BASE_PATH}/snapshots`);
- } else if (repository && repository !== filteredRepository) {
- setFilteredRepository(String(repository));
+ } else if (repository) {
+ setListParams((prev: SnapshotListParams) => ({
+ ...prev,
+ searchField: 'repository',
+ searchValue: String(repository),
+ searchMatch: 'must',
+ searchOperator: 'exact',
+ }));
history.replace(`${BASE_PATH}/snapshots`);
}
}
- }, [filteredPolicy, filteredRepository, history, search]);
+ }, [listParams, history, search]);
// Track component loaded
useEffect(() => {
@@ -110,7 +109,8 @@ export const SnapshotList: React.FunctionComponent
@@ -134,190 +134,11 @@ export const SnapshotList: React.FunctionComponent
);
} else if (Object.keys(errors).length && repositories.length === 0) {
- content = (
-
-
-
-
- }
- body={
-
-
-
-
- ),
- }}
- />
-
- }
- />
-
- );
+ content = ;
} else if (repositories.length === 0) {
- content = (
-
-
-
-
- }
- body={
- <>
-
-
-
-
-
-
-
-
- >
- }
- data-test-subj="emptyPrompt"
- />
-
- );
- } else if (snapshots.length === 0) {
- content = (
-
-
-
-
- }
- body={
- `cluster.${name}`)}
- >
- {({ hasPrivileges }) =>
- hasPrivileges ? (
-
-
-
-
-
- ),
- }}
- />
-
-
- {policies.length === 0 ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
- ) : (
-
-
-
-
-
-
- {' '}
-
-
-
-
- )
- }
-
- }
- data-test-subj="emptyPrompt"
- />
-
- );
+ content = ;
+ } else if (totalSnapshotsCount === 0 && !listParams.searchField && !isLoading) {
+ content = ;
} else {
const repositoryErrorsWarning = Object.keys(errors).length ? (
<>
@@ -351,53 +172,19 @@ export const SnapshotList: React.FunctionComponent
) : null;
- const maxSnapshotsWarning = snapshots.length === SNAPSHOT_LIST_MAX_SIZE && (
- <>
-
-
-
-
- ),
- }}
- />
-
-
- >
- );
-
content = (
{repositoryErrorsWarning}
- {maxSnapshotsWarning}
-
);
diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/snapshot_requests.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/snapshot_requests.ts
index 3d64dc96958de..c02d0f053f783 100644
--- a/x-pack/plugins/snapshot_restore/public/application/services/http/snapshot_requests.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/services/http/snapshot_requests.ts
@@ -5,8 +5,10 @@
* 2.0.
*/
-import { API_BASE_PATH } from '../../../../common/constants';
+import { HttpFetchQuery } from 'kibana/public';
+import { API_BASE_PATH } from '../../../../common';
import { UIM_SNAPSHOT_DELETE, UIM_SNAPSHOT_DELETE_MANY } from '../../constants';
+import { SnapshotListParams } from '../../lib';
import { UiMetricService } from '../ui_metric';
import { sendRequest, useRequest } from './use_request';
@@ -18,11 +20,12 @@ export const setUiMetricServiceSnapshot = (_uiMetricService: UiMetricService) =>
};
// End hack
-export const useLoadSnapshots = () =>
+export const useLoadSnapshots = (query: SnapshotListParams) =>
useRequest({
path: `${API_BASE_PATH}snapshots`,
method: 'get',
initialData: [],
+ query: query as unknown as HttpFetchQuery,
});
export const useLoadSnapshot = (repositoryName: string, snapshotId: string) =>
diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts
index d1b9f37703c0c..a3cda90d26f2a 100644
--- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts
+++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts
@@ -26,3 +26,5 @@ export {
} from '../../../../src/plugins/es_ui_shared/public';
export { APP_WRAPPER_CLASS } from '../../../../src/core/public';
+
+export { reactRouterNavigate } from '../../../../src/plugins/kibana_react/public';
diff --git a/x-pack/plugins/snapshot_restore/server/lib/get_snapshot_search_wildcard.test.ts b/x-pack/plugins/snapshot_restore/server/lib/get_snapshot_search_wildcard.test.ts
new file mode 100644
index 0000000000000..d3e5c604d22ad
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/server/lib/get_snapshot_search_wildcard.test.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getSnapshotSearchWildcard } from './get_snapshot_search_wildcard';
+
+describe('getSnapshotSearchWildcard', () => {
+ it('exact match search converts to a wildcard without *', () => {
+ const searchParams = {
+ field: 'snapshot',
+ value: 'testSearch',
+ operator: 'exact',
+ match: 'must',
+ };
+ const wildcard = getSnapshotSearchWildcard(searchParams);
+ expect(wildcard).toEqual('testSearch');
+ });
+
+ it('partial match search converts to a wildcard with *', () => {
+ const searchParams = { field: 'snapshot', value: 'testSearch', operator: 'eq', match: 'must' };
+ const wildcard = getSnapshotSearchWildcard(searchParams);
+ expect(wildcard).toEqual('*testSearch*');
+ });
+
+ it('excluding search converts to "all, except" wildcard (exact match)', () => {
+ const searchParams = {
+ field: 'snapshot',
+ value: 'testSearch',
+ operator: 'exact',
+ match: 'must_not',
+ };
+ const wildcard = getSnapshotSearchWildcard(searchParams);
+ expect(wildcard).toEqual('*,-testSearch');
+ });
+
+ it('excluding search converts to "all, except" wildcard (partial match)', () => {
+ const searchParams = {
+ field: 'snapshot',
+ value: 'testSearch',
+ operator: 'eq',
+ match: 'must_not',
+ };
+ const wildcard = getSnapshotSearchWildcard(searchParams);
+ expect(wildcard).toEqual('*,-*testSearch*');
+ });
+
+ it('excluding search for policy name converts to "all,_none, except" wildcard', () => {
+ const searchParams = {
+ field: 'policyName',
+ value: 'testSearch',
+ operator: 'exact',
+ match: 'must_not',
+ };
+ const wildcard = getSnapshotSearchWildcard(searchParams);
+ expect(wildcard).toEqual('*,_none,-testSearch');
+ });
+});
diff --git a/x-pack/plugins/snapshot_restore/server/lib/get_snapshot_search_wildcard.ts b/x-pack/plugins/snapshot_restore/server/lib/get_snapshot_search_wildcard.ts
new file mode 100644
index 0000000000000..df8926d785712
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/server/lib/get_snapshot_search_wildcard.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+interface SearchParams {
+ field: string;
+ value: string;
+ match?: string;
+ operator?: string;
+}
+
+export const getSnapshotSearchWildcard = ({
+ field,
+ value,
+ match,
+ operator,
+}: SearchParams): string => {
+ // if the operator is NOT for exact match, convert to *value* wildcard that matches any substring
+ value = operator === 'exact' ? value : `*${value}*`;
+
+ // ES API new "-"("except") wildcard removes matching items from a list of already selected items
+ // To find all items not containing the search value, use "*,-{searchValue}"
+ // When searching for policy name, also add "_none" to find snapshots without a policy as well
+ const excludingWildcard = field === 'policyName' ? `*,_none,-${value}` : `*,-${value}`;
+
+ return match === 'must_not' ? excludingWildcard : value;
+};
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
index f71c5ec9ffc08..4ecd34a43adb9 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
@@ -51,6 +51,10 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
const mockRequest: RequestMock = {
method: 'get',
path: addBasePath('snapshots'),
+ query: {
+ sortField: 'startTimeInMillis',
+ sortDirection: 'desc',
+ },
};
const mockSnapshotGetManagedRepositoryEsResponse = {
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
index 6838ae2700f3a..4de0c3011fed5 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
@@ -7,11 +7,36 @@
import { schema, TypeOf } from '@kbn/config-schema';
import type { SnapshotDetailsEs } from '../../../common/types';
-import { SNAPSHOT_LIST_MAX_SIZE } from '../../../common/constants';
import { deserializeSnapshotDetails } from '../../../common/lib';
import type { RouteDependencies } from '../../types';
import { getManagedRepositoryName } from '../../lib';
import { addBasePath } from '../helpers';
+import { snapshotListSchema } from './validate_schemas';
+import { getSnapshotSearchWildcard } from '../../lib/get_snapshot_search_wildcard';
+
+const sortFieldToESParams = {
+ snapshot: 'name',
+ repository: 'repository',
+ indices: 'index_count',
+ startTimeInMillis: 'start_time',
+ durationInMillis: 'duration',
+ 'shards.total': 'shard_count',
+ 'shards.failed': 'failed_shard_count',
+};
+
+const isSearchingForNonExistentRepository = (
+ repositories: string[],
+ value: string,
+ match?: string,
+ operator?: string
+): boolean => {
+ // only check if searching for an exact match (repository=test)
+ if (match === 'must' && operator === 'exact') {
+ return !(repositories || []).includes(value);
+ }
+ // otherwise we will use a wildcard, so allow the request
+ return false;
+};
export function registerSnapshotsRoutes({
router,
@@ -20,9 +45,18 @@ export function registerSnapshotsRoutes({
}: RouteDependencies) {
// GET all snapshots
router.get(
- { path: addBasePath('snapshots'), validate: false },
+ { path: addBasePath('snapshots'), validate: { query: snapshotListSchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { client: clusterClient } = ctx.core.elasticsearch;
+ const sortField =
+ sortFieldToESParams[(req.query as TypeOf).sortField];
+ const sortDirection = (req.query as TypeOf).sortDirection;
+ const pageIndex = (req.query as TypeOf).pageIndex;
+ const pageSize = (req.query as TypeOf).pageSize;
+ const searchField = (req.query as TypeOf).searchField;
+ const searchValue = (req.query as TypeOf).searchValue;
+ const searchMatch = (req.query as TypeOf).searchMatch;
+ const searchOperator = (req.query as TypeOf).searchOperator;
const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser);
@@ -55,18 +89,60 @@ export function registerSnapshotsRoutes({
return handleEsError({ error: e, response: res });
}
+ // if the search is for a repository name with exact match (repository=test)
+ // and that repository doesn't exist, ES request throws an error
+ // that is why we return an empty snapshots array instead of sending an ES request
+ if (
+ searchField === 'repository' &&
+ isSearchingForNonExistentRepository(repositories, searchValue!, searchMatch, searchOperator)
+ ) {
+ return res.ok({
+ body: {
+ snapshots: [],
+ policies,
+ repositories,
+ errors: [],
+ total: 0,
+ },
+ });
+ }
try {
// If any of these repositories 504 they will cost the request significant time.
const { body: fetchedSnapshots } = await clusterClient.asCurrentUser.snapshot.get({
- repository: '_all',
- snapshot: '_all',
+ repository:
+ searchField === 'repository'
+ ? getSnapshotSearchWildcard({
+ field: searchField,
+ value: searchValue!,
+ match: searchMatch,
+ operator: searchOperator,
+ })
+ : '_all',
ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable.
- // @ts-expect-error @elastic/elasticsearch "desc" is a new param
- order: 'desc',
- // TODO We are temporarily hard-coding the maximum number of snapshots returned
- // in order to prevent an unusable UI for users with large number of snapshots
- // In the near future, this will be resolved with server-side pagination
- size: SNAPSHOT_LIST_MAX_SIZE,
+ snapshot:
+ searchField === 'snapshot'
+ ? getSnapshotSearchWildcard({
+ field: searchField,
+ value: searchValue!,
+ match: searchMatch,
+ operator: searchOperator,
+ })
+ : '_all',
+ // @ts-expect-error @elastic/elasticsearch new API params
+ // https://github.com/elastic/elasticsearch-specification/issues/845
+ slm_policy_filter:
+ searchField === 'policyName'
+ ? getSnapshotSearchWildcard({
+ field: searchField,
+ value: searchValue!,
+ match: searchMatch,
+ operator: searchOperator,
+ })
+ : '*,_none',
+ order: sortDirection,
+ sort: sortField,
+ size: pageSize,
+ offset: pageIndex * pageSize,
});
// Decorate each snapshot with the repository with which it's associated.
@@ -79,8 +155,10 @@ export function registerSnapshotsRoutes({
snapshots: snapshots || [],
policies,
repositories,
- // @ts-expect-error @elastic/elasticsearch "failures" is a new field in the response
+ // @ts-expect-error @elastic/elasticsearch https://github.com/elastic/elasticsearch-specification/issues/845
errors: fetchedSnapshots?.failures,
+ // @ts-expect-error @elastic/elasticsearch "total" is a new field in the response
+ total: fetchedSnapshots?.total,
},
});
} catch (e) {
@@ -170,7 +248,7 @@ export function registerSnapshotsRoutes({
const snapshots = req.body;
try {
- // We intentially perform deletion requests sequentially (blocking) instead of in parallel (non-blocking)
+ // We intentionally perform deletion requests sequentially (blocking) instead of in parallel (non-blocking)
// because there can only be one snapshot deletion task performed at a time (ES restriction).
for (let i = 0; i < snapshots.length; i++) {
const { snapshot, repository } = snapshots[i];
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts
index af31466c2cefe..e93ee2b3d78ca 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts
@@ -26,6 +26,31 @@ const snapshotRetentionSchema = schema.object({
minCount: schema.maybe(schema.oneOf([schema.number(), schema.literal('')])),
});
+export const snapshotListSchema = schema.object({
+ sortField: schema.oneOf([
+ schema.literal('snapshot'),
+ schema.literal('repository'),
+ schema.literal('indices'),
+ schema.literal('durationInMillis'),
+ schema.literal('startTimeInMillis'),
+ schema.literal('shards.total'),
+ schema.literal('shards.failed'),
+ ]),
+ sortDirection: schema.oneOf([schema.literal('desc'), schema.literal('asc')]),
+ pageIndex: schema.number(),
+ pageSize: schema.number(),
+ searchField: schema.maybe(
+ schema.oneOf([
+ schema.literal('snapshot'),
+ schema.literal('repository'),
+ schema.literal('policyName'),
+ ])
+ ),
+ searchValue: schema.maybe(schema.string()),
+ searchMatch: schema.maybe(schema.oneOf([schema.literal('must'), schema.literal('must_not')])),
+ searchOperator: schema.maybe(schema.oneOf([schema.literal('eq'), schema.literal('exact')])),
+});
+
export const policySchema = schema.object({
name: schema.string(),
snapshotName: schema.string(),
diff --git a/x-pack/plugins/snapshot_restore/server/routes/helpers.ts b/x-pack/plugins/snapshot_restore/server/routes/helpers.ts
index 1f49d2f3cabfb..e73db4d992ff2 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/helpers.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/helpers.ts
@@ -5,6 +5,6 @@
* 2.0.
*/
-import { API_BASE_PATH } from '../../common/constants';
+import { API_BASE_PATH } from '../../common';
export const addBasePath = (uri: string): string => API_BASE_PATH + uri;
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 41cc7825bcf36..fc89eb25bfc91 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -23904,9 +23904,6 @@
"xpack.snapshotRestore.snapshotList.table.snapshotColumnTitle": "スナップショット",
"xpack.snapshotRestore.snapshotList.table.startTimeColumnTitle": "日付が作成されました",
"xpack.snapshotRestore.snapshots.breadcrumbTitle": "スナップショット",
- "xpack.snapshotRestore.snapshotsList.maxSnapshotsDisplayedDescription": "表示可能なスナップショットの最大数に達しました。スナップショットをすべて表示するには、{docLink}を使用してください。",
- "xpack.snapshotRestore.snapshotsList.maxSnapshotsDisplayedDocLinkText": "Elasticsearch API",
- "xpack.snapshotRestore.snapshotsList.maxSnapshotsDisplayedTitle": "スナップショットの一覧を表示できません。",
"xpack.snapshotRestore.snapshotState.completeLabel": "スナップショット完了",
"xpack.snapshotRestore.snapshotState.failedLabel": "スナップショット失敗",
"xpack.snapshotRestore.snapshotState.incompatibleLabel": "互換性のないバージョン",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 54811936957ab..a85cc7b60b00b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -24306,9 +24306,6 @@
"xpack.snapshotRestore.snapshotList.table.snapshotColumnTitle": "快照",
"xpack.snapshotRestore.snapshotList.table.startTimeColumnTitle": "创建日期",
"xpack.snapshotRestore.snapshots.breadcrumbTitle": "快照",
- "xpack.snapshotRestore.snapshotsList.maxSnapshotsDisplayedDescription": "已达到最大可查看快照数目。要查看您的所有快照,请使用{docLink}。",
- "xpack.snapshotRestore.snapshotsList.maxSnapshotsDisplayedDocLinkText": "Elasticsearch API",
- "xpack.snapshotRestore.snapshotsList.maxSnapshotsDisplayedTitle": "无法显示快照的完整列表",
"xpack.snapshotRestore.snapshotState.completeLabel": "快照完成",
"xpack.snapshotRestore.snapshotState.failedLabel": "快照失败",
"xpack.snapshotRestore.snapshotState.incompatibleLabel": "不兼容版本",
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts
index 4d39ff1494f89..db5dbc9735e66 100644
--- a/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts
@@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Snapshot and Restore', () => {
- loadTestFile(require.resolve('./snapshot_restore'));
+ loadTestFile(require.resolve('./policies'));
+ loadTestFile(require.resolve('./snapshots'));
});
}
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts
index 9b4d39a3b10b3..a59c90fe29132 100644
--- a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts
@@ -7,9 +7,10 @@
import { FtrProviderContext } from '../../../../ftr_provider_context';
-interface SlmPolicy {
+export interface SlmPolicy {
+ policyName: string;
+ // snapshot name
name: string;
- snapshotName: string;
schedule: string;
repository: string;
isManagedPolicy: boolean;
@@ -29,23 +30,22 @@ interface SlmPolicy {
}
/**
- * Helpers to create and delete SLM policies on the Elasticsearch instance
+ * Helpers to create and delete SLM policies, repositories and snapshots on the Elasticsearch instance
* during our tests.
- * @param {ElasticsearchClient} es The Elasticsearch client instance
*/
export const registerEsHelpers = (getService: FtrProviderContext['getService']) => {
let policiesCreated: string[] = [];
const es = getService('es');
- const createRepository = (repoName: string) => {
+ const createRepository = (repoName: string, repoPath?: string) => {
return es.snapshot
.createRepository({
repository: repoName,
body: {
type: 'fs',
settings: {
- location: '/tmp/',
+ location: repoPath ?? '/tmp/repo',
},
},
verify: false,
@@ -55,12 +55,12 @@ export const registerEsHelpers = (getService: FtrProviderContext['getService'])
const createPolicy = (policy: SlmPolicy, cachePolicy?: boolean) => {
if (cachePolicy) {
- policiesCreated.push(policy.name);
+ policiesCreated.push(policy.policyName);
}
return es.slm
.putLifecycle({
- policy_id: policy.name,
+ policy_id: policy.policyName,
// TODO: bring {@link SlmPolicy} in line with {@link PutSnapshotLifecycleRequest['body']}
// @ts-expect-error
body: policy,
@@ -90,11 +90,34 @@ export const registerEsHelpers = (getService: FtrProviderContext['getService'])
console.log(`[Cleanup error] Error deleting ES resources: ${err.message}`);
});
+ const executePolicy = (policyName: string) => {
+ return es.slm.executeLifecycle({ policy_id: policyName }).then(({ body }) => body);
+ };
+
+ const createSnapshot = (snapshotName: string, repositoryName: string) => {
+ return es.snapshot
+ .create({ snapshot: snapshotName, repository: repositoryName })
+ .then(({ body }) => body);
+ };
+
+ const deleteSnapshots = (repositoryName: string) => {
+ es.snapshot
+ .delete({ repository: repositoryName, snapshot: '*' })
+ .then(() => {})
+ .catch((err) => {
+ // eslint-disable-next-line no-console
+ console.log(`[Cleanup error] Error deleting snapshots: ${err.message}`);
+ });
+ };
+
return {
createRepository,
createPolicy,
deletePolicy,
cleanupPolicies,
getPolicy,
+ executePolicy,
+ createSnapshot,
+ deleteSnapshots,
};
};
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts
index 27a4d9c59cff0..a9721c5856598 100644
--- a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { registerEsHelpers } from './elasticsearch';
+export { registerEsHelpers, SlmPolicy } from './elasticsearch';
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/policies.ts
similarity index 95%
rename from x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts
rename to x-pack/test/api_integration/apis/management/snapshot_restore/policies.ts
index a6ac2d057c84e..e0734680887d2 100644
--- a/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/policies.ts
@@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) {
const { createRepository, createPolicy, deletePolicy, cleanupPolicies, getPolicy } =
registerEsHelpers(getService);
- describe('Snapshot Lifecycle Management', function () {
+ describe('SLM policies', function () {
this.tags(['skipCloud']); // file system repositories are not supported in cloud
before(async () => {
@@ -134,9 +134,8 @@ export default function ({ getService }: FtrProviderContext) {
describe('Update', () => {
const POLICY_NAME = 'test_update_policy';
+ const SNAPSHOT_NAME = 'my_snapshot';
const POLICY = {
- name: POLICY_NAME,
- snapshotName: 'my_snapshot',
schedule: '0 30 1 * * ?',
repository: REPO_NAME,
config: {
@@ -159,7 +158,7 @@ export default function ({ getService }: FtrProviderContext) {
before(async () => {
// Create SLM policy that can be used to test PUT request
try {
- await createPolicy(POLICY, true);
+ await createPolicy({ ...POLICY, policyName: POLICY_NAME, name: SNAPSHOT_NAME }, true);
} catch (err) {
// eslint-disable-next-line no-console
console.log('[Setup error] Error creating policy');
@@ -175,6 +174,8 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'xxx')
.send({
...POLICY,
+ name: POLICY_NAME,
+ snapshotName: SNAPSHOT_NAME,
schedule: '0 0 0 ? * 7',
})
.expect(200);
@@ -212,7 +213,7 @@ export default function ({ getService }: FtrProviderContext) {
const { body } = await supertest
.put(uri)
.set('kbn-xsrf', 'xxx')
- .send(requiredFields)
+ .send({ ...requiredFields, name: POLICY_NAME, snapshotName: SNAPSHOT_NAME })
.expect(200);
expect(body).to.eql({
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/snapshots.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshots.ts
new file mode 100644
index 0000000000000..1677013dd5e7e
--- /dev/null
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshots.ts
@@ -0,0 +1,729 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { registerEsHelpers, SlmPolicy } from './lib';
+import { SnapshotDetails } from '../../../../../plugins/snapshot_restore/common/types';
+
+const REPO_NAME_1 = 'test_repo_1';
+const REPO_NAME_2 = 'test_another_repo_2';
+const REPO_PATH_1 = '/tmp/repo_1';
+const REPO_PATH_2 = '/tmp/repo_2';
+// SLM policies to test policyName filter
+const POLICY_NAME_1 = 'test_policy_1';
+const POLICY_NAME_2 = 'test_another_policy_2';
+const POLICY_SNAPSHOT_NAME_1 = 'backup_snapshot';
+const POLICY_SNAPSHOT_NAME_2 = 'a_snapshot';
+// snapshots created without SLM policies
+const BATCH_SIZE_1 = 3;
+const BATCH_SIZE_2 = 5;
+const BATCH_SNAPSHOT_NAME_1 = 'another_snapshot';
+const BATCH_SNAPSHOT_NAME_2 = 'xyz_another_snapshot';
+// total count consists of both batches' sizes + 2 snapshots created by 2 SLM policies (one each)
+const SNAPSHOT_COUNT = BATCH_SIZE_1 + BATCH_SIZE_2 + 2;
+// API defaults used in the UI
+const PAGE_INDEX = 0;
+const PAGE_SIZE = 20;
+const SORT_FIELD = 'startTimeInMillis';
+const SORT_DIRECTION = 'desc';
+
+interface ApiParams {
+ pageIndex?: number;
+ pageSize?: number;
+
+ sortField?: string;
+ sortDirection?: string;
+
+ searchField?: string;
+ searchValue?: string;
+ searchMatch?: string;
+ searchOperator?: string;
+}
+const getApiPath = ({
+ pageIndex,
+ pageSize,
+ sortField,
+ sortDirection,
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+}: ApiParams): string => {
+ let path = `/api/snapshot_restore/snapshots?sortField=${sortField ?? SORT_FIELD}&sortDirection=${
+ sortDirection ?? SORT_DIRECTION
+ }&pageIndex=${pageIndex ?? PAGE_INDEX}&pageSize=${pageSize ?? PAGE_SIZE}`;
+ // all 4 parameters should be used at the same time to configure the correct search request
+ if (searchField && searchValue && searchMatch && searchOperator) {
+ path = `${path}&searchField=${searchField}&searchValue=${searchValue}&searchMatch=${searchMatch}&searchOperator=${searchOperator}`;
+ }
+ return path;
+};
+const getPolicyBody = (policy: Partial): SlmPolicy => {
+ return {
+ policyName: 'default_policy',
+ name: 'default_snapshot',
+ schedule: '0 30 1 * * ?',
+ repository: 'default_repo',
+ isManagedPolicy: false,
+ config: {
+ indices: ['default_index'],
+ ignoreUnavailable: true,
+ },
+ ...policy,
+ };
+};
+
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+
+ const {
+ createSnapshot,
+ createRepository,
+ createPolicy,
+ executePolicy,
+ cleanupPolicies,
+ deleteSnapshots,
+ } = registerEsHelpers(getService);
+
+ describe('Snapshots', function () {
+ this.tags(['skipCloud']); // file system repositories are not supported in cloud
+
+ // names of snapshots created by SLM policies have random suffixes, save full names for tests
+ let snapshotName1: string;
+ let snapshotName2: string;
+
+ before(async () => {
+ /*
+ * This setup creates following repos, SLM policies and snapshots:
+ * Repo 1 "test_repo_1" with 5 snapshots
+ * "backup_snapshot..." (created by SLM policy "test_policy_1")
+ * "a_snapshot..." (created by SLM policy "test_another_policy_2")
+ * "another_snapshot_0" to "another_snapshot_2" (no SLM policy)
+ *
+ * Repo 2 "test_another_repo_2" with 5 snapshots
+ * "xyz_another_snapshot_0" to "xyz_another_snapshot_4" (no SLM policy)
+ */
+ try {
+ await createRepository(REPO_NAME_1, REPO_PATH_1);
+ await createRepository(REPO_NAME_2, REPO_PATH_2);
+ await createPolicy(
+ getPolicyBody({
+ policyName: POLICY_NAME_1,
+ repository: REPO_NAME_1,
+ name: POLICY_SNAPSHOT_NAME_1,
+ }),
+ true
+ );
+ await createPolicy(
+ getPolicyBody({
+ policyName: POLICY_NAME_2,
+ repository: REPO_NAME_1,
+ name: POLICY_SNAPSHOT_NAME_2,
+ }),
+ true
+ );
+ ({ snapshot_name: snapshotName1 } = await executePolicy(POLICY_NAME_1));
+ // a short timeout to let the 1st snapshot start, otherwise the sorting by start time might be flaky
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ ({ snapshot_name: snapshotName2 } = await executePolicy(POLICY_NAME_2));
+ for (let i = 0; i < BATCH_SIZE_1; i++) {
+ await createSnapshot(`${BATCH_SNAPSHOT_NAME_1}_${i}`, REPO_NAME_1);
+ }
+ for (let i = 0; i < BATCH_SIZE_2; i++) {
+ await createSnapshot(`${BATCH_SNAPSHOT_NAME_2}_${i}`, REPO_NAME_2);
+ }
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log('[Setup error] Error creating snapshots');
+ throw err;
+ }
+ });
+
+ after(async () => {
+ await cleanupPolicies();
+ await deleteSnapshots(REPO_NAME_1);
+ await deleteSnapshots(REPO_NAME_2);
+ });
+
+ describe('pagination', () => {
+ it('returns pageSize number of snapshots', async () => {
+ const pageSize = 7;
+ const {
+ body: { total, snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ pageSize,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+ expect(total).to.eql(SNAPSHOT_COUNT);
+ expect(snapshots.length).to.eql(pageSize);
+ });
+
+ it('returns next page of snapshots', async () => {
+ const pageSize = 3;
+ let pageIndex = 0;
+ const {
+ body: { snapshots: firstPageSnapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ pageIndex,
+ pageSize,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ const firstPageSnapshotName = firstPageSnapshots[0].snapshot;
+ expect(firstPageSnapshots.length).to.eql(pageSize);
+
+ pageIndex = 1;
+ const {
+ body: { snapshots: secondPageSnapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ pageIndex,
+ pageSize,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ const secondPageSnapshotName = secondPageSnapshots[0].snapshot;
+ expect(secondPageSnapshots.length).to.eql(pageSize);
+ expect(secondPageSnapshotName).to.not.eql(firstPageSnapshotName);
+ });
+ });
+
+ describe('sorting', () => {
+ it('sorts by snapshot name (asc)', async () => {
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ sortField: 'snapshot',
+ sortDirection: 'asc',
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ /*
+ * snapshots name in asc order:
+ * "a_snapshot...", "another_snapshot...", "backup_snapshot...", "xyz_another_snapshot..."
+ */
+ const snapshotName = snapshots[0].snapshot;
+ // snapshotName2 is "a_snapshot..."
+ expect(snapshotName).to.eql(snapshotName2);
+ });
+
+ it('sorts by snapshot name (desc)', async () => {
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ sortField: 'snapshot',
+ sortDirection: 'desc',
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+ /*
+ * snapshots name in desc order:
+ * "xyz_another_snapshot...", "backup_snapshot...", "another_snapshot...", "a_snapshot..."
+ */
+ const snapshotName = snapshots[0].snapshot;
+ expect(snapshotName).to.eql('xyz_another_snapshot_4');
+ });
+
+ it('sorts by repository name (asc)', async () => {
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ sortField: 'repository',
+ sortDirection: 'asc',
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+ // repositories in asc order: "test_another_repo_2", "test_repo_1"
+ const repositoryName = snapshots[0].repository;
+ expect(repositoryName).to.eql(REPO_NAME_2); // "test_another_repo_2"
+ });
+
+ it('sorts by repository name (desc)', async () => {
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ sortField: 'repository',
+ sortDirection: 'desc',
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+ // repositories in desc order: "test_repo_1", "test_another_repo_2"
+ const repositoryName = snapshots[0].repository;
+ expect(repositoryName).to.eql(REPO_NAME_1); // "test_repo_1"
+ });
+
+ it('sorts by startTimeInMillis (asc)', async () => {
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ sortField: 'startTimeInMillis',
+ sortDirection: 'asc',
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+ const snapshotName = snapshots[0].snapshot;
+ // the 1st snapshot that was created during this test setup
+ expect(snapshotName).to.eql(snapshotName1);
+ });
+
+ it('sorts by startTimeInMillis (desc)', async () => {
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ sortField: 'startTimeInMillis',
+ sortDirection: 'desc',
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+ const snapshotName = snapshots[0].snapshot;
+ // the last snapshot that was created during this test setup
+ expect(snapshotName).to.eql('xyz_another_snapshot_4');
+ });
+
+ // these properties are only tested as being accepted by the API
+ const sortFields = ['indices', 'durationInMillis', 'shards.total', 'shards.failed'];
+ sortFields.forEach((sortField: string) => {
+ it(`allows sorting by ${sortField} (asc)`, async () => {
+ await supertest
+ .get(
+ getApiPath({
+ sortField,
+ sortDirection: 'asc',
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send()
+ .expect(200);
+ });
+
+ it(`allows sorting by ${sortField} (desc)`, async () => {
+ await supertest
+ .get(
+ getApiPath({
+ sortField,
+ sortDirection: 'desc',
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send()
+ .expect(200);
+ });
+ });
+ });
+
+ describe('search', () => {
+ describe('snapshot name', () => {
+ it('exact match', async () => {
+ // list snapshots with the name "another_snapshot_2"
+ const searchField = 'snapshot';
+ const searchValue = 'another_snapshot_2';
+ const searchMatch = 'must';
+ const searchOperator = 'exact';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ expect(snapshots.length).to.eql(1);
+ expect(snapshots[0].snapshot).to.eql('another_snapshot_2');
+ });
+
+ it('partial match', async () => {
+ // list snapshots with the name containing with "another"
+ const searchField = 'snapshot';
+ const searchValue = 'another';
+ const searchMatch = 'must';
+ const searchOperator = 'eq';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ // both batches created snapshots containing "another" in the name
+ expect(snapshots.length).to.eql(BATCH_SIZE_1 + BATCH_SIZE_2);
+ const snapshotNamesContainSearch = snapshots.every((snapshot: SnapshotDetails) =>
+ snapshot.snapshot.includes('another')
+ );
+ expect(snapshotNamesContainSearch).to.eql(true);
+ });
+
+ it('excluding search with exact match', async () => {
+ // list snapshots with the name not "another_snapshot_2"
+ const searchField = 'snapshot';
+ const searchValue = 'another_snapshot_2';
+ const searchMatch = 'must_not';
+ const searchOperator = 'exact';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ expect(snapshots.length).to.eql(SNAPSHOT_COUNT - 1);
+ const snapshotIsExcluded = snapshots.every(
+ (snapshot: SnapshotDetails) => snapshot.snapshot !== 'another_snapshot_2'
+ );
+ expect(snapshotIsExcluded).to.eql(true);
+ });
+
+ it('excluding search with partial match', async () => {
+ // list snapshots with the name not starting with "another"
+ const searchField = 'snapshot';
+ const searchValue = 'another';
+ const searchMatch = 'must_not';
+ const searchOperator = 'eq';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ // both batches created snapshots with names containing "another"
+ expect(snapshots.length).to.eql(SNAPSHOT_COUNT - BATCH_SIZE_1 - BATCH_SIZE_2);
+ const snapshotsAreExcluded = snapshots.every(
+ (snapshot: SnapshotDetails) => !snapshot.snapshot.includes('another')
+ );
+ expect(snapshotsAreExcluded).to.eql(true);
+ });
+ });
+
+ describe('repository name', () => {
+ it('search for non-existent repository returns an empty snapshot array', async () => {
+ // search for non-existent repository
+ const searchField = 'repository';
+ const searchValue = 'non-existent';
+ const searchMatch = 'must';
+ const searchOperator = 'exact';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ expect(snapshots.length).to.eql(0);
+ });
+
+ it('exact match', async () => {
+ // list snapshots from repository "test_repo_1"
+ const searchField = 'repository';
+ const searchValue = REPO_NAME_1;
+ const searchMatch = 'must';
+ const searchOperator = 'exact';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ // repo 1 contains snapshots from batch 1 and 2 snapshots created by 2 SLM policies
+ expect(snapshots.length).to.eql(BATCH_SIZE_1 + 2);
+ const repositoryNameMatches = snapshots.every(
+ (snapshot: SnapshotDetails) => snapshot.repository === REPO_NAME_1
+ );
+ expect(repositoryNameMatches).to.eql(true);
+ });
+
+ it('partial match', async () => {
+ // list snapshots from repository with the name containing "another"
+ // i.e. snapshots from repo 2
+ const searchField = 'repository';
+ const searchValue = 'another';
+ const searchMatch = 'must';
+ const searchOperator = 'eq';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ // repo 2 only contains snapshots created by batch 2
+ expect(snapshots.length).to.eql(BATCH_SIZE_2);
+ const repositoryNameMatches = snapshots.every((snapshot: SnapshotDetails) =>
+ snapshot.repository.includes('another')
+ );
+ expect(repositoryNameMatches).to.eql(true);
+ });
+
+ it('excluding search with exact match', async () => {
+ // list snapshots from repositories with the name not "test_repo_1"
+ const searchField = 'repository';
+ const searchValue = REPO_NAME_1;
+ const searchMatch = 'must_not';
+ const searchOperator = 'exact';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ // snapshots not in repo 1 are only snapshots created in batch 2
+ expect(snapshots.length).to.eql(BATCH_SIZE_2);
+ const repositoryNameMatches = snapshots.every(
+ (snapshot: SnapshotDetails) => snapshot.repository !== REPO_NAME_1
+ );
+ expect(repositoryNameMatches).to.eql(true);
+ });
+
+ it('excluding search with partial match', async () => {
+ // list snapshots from repository with the name not containing "test"
+ const searchField = 'repository';
+ const searchValue = 'test';
+ const searchMatch = 'must_not';
+ const searchOperator = 'eq';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ expect(snapshots.length).to.eql(0);
+ });
+ });
+
+ describe('policy name', () => {
+ it('search for non-existent policy returns an empty snapshot array', async () => {
+ // search for non-existent policy
+ const searchField = 'policyName';
+ const searchValue = 'non-existent';
+ const searchMatch = 'must';
+ const searchOperator = 'exact';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ expect(snapshots.length).to.eql(0);
+ });
+
+ it('exact match', async () => {
+ // list snapshots created by the policy "test_policy_1"
+ const searchField = 'policyName';
+ const searchValue = POLICY_NAME_1;
+ const searchMatch = 'must';
+ const searchOperator = 'exact';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ expect(snapshots.length).to.eql(1);
+ expect(snapshots[0].policyName).to.eql(POLICY_NAME_1);
+ });
+
+ it('partial match', async () => {
+ // list snapshots created by the policy with the name containing "another"
+ const searchField = 'policyName';
+ const searchValue = 'another';
+ const searchMatch = 'must';
+ const searchOperator = 'eq';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ // 1 snapshot was created by the policy "test_another_policy_2"
+ expect(snapshots.length).to.eql(1);
+ const policyNameMatches = snapshots.every((snapshot: SnapshotDetails) =>
+ snapshot.policyName!.includes('another')
+ );
+ expect(policyNameMatches).to.eql(true);
+ });
+
+ it('excluding search with exact match', async () => {
+ // list snapshots created by the policy with the name not "test_policy_1"
+ const searchField = 'policyName';
+ const searchValue = POLICY_NAME_1;
+ const searchMatch = 'must_not';
+ const searchOperator = 'exact';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ // only 1 snapshot was created by policy 1
+ // search results should also contain snapshots without SLM policy
+ expect(snapshots.length).to.eql(SNAPSHOT_COUNT - 1);
+ const snapshotsExcluded = snapshots.every(
+ (snapshot: SnapshotDetails) => (snapshot.policyName ?? '') !== POLICY_NAME_1
+ );
+ expect(snapshotsExcluded).to.eql(true);
+ });
+
+ it('excluding search with partial match', async () => {
+ // list snapshots created by the policy with the name not containing "another"
+ const searchField = 'policyName';
+ const searchValue = 'another';
+ const searchMatch = 'must_not';
+ const searchOperator = 'eq';
+ const {
+ body: { snapshots },
+ } = await supertest
+ .get(
+ getApiPath({
+ searchField,
+ searchValue,
+ searchMatch,
+ searchOperator,
+ })
+ )
+ .set('kbn-xsrf', 'xxx')
+ .send();
+
+ // only 1 snapshot was created by SLM policy containing "another" in the name
+ // search results should also contain snapshots without SLM policy
+ expect(snapshots.length).to.eql(SNAPSHOT_COUNT - 1);
+ const snapshotsExcluded = snapshots.every(
+ (snapshot: SnapshotDetails) => !(snapshot.policyName ?? '').includes('another')
+ );
+ expect(snapshotsExcluded).to.eql(true);
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts
index 3690f661c621c..678f7a0d3d929 100644
--- a/x-pack/test/api_integration/config.ts
+++ b/x-pack/test/api_integration/config.ts
@@ -41,6 +41,7 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi
serverArgs: [
...xPackFunctionalTestsConfig.get('esTestCluster.serverArgs'),
'node.attr.name=apiIntegrationTestNode',
+ 'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2',
],
},
};