Skip to content

Commit

Permalink
[Snapshot and Restore] Server side snapshots pagination (#110266)
Browse files Browse the repository at this point in the history
* [Snapshot % Restore] Added server side pagination and sorting to get snapshots route, refactored snapshots table to use EuiBasicTable with controlled pagination instead of EuiInMemoryTable

* [Snapshot & Restore] Added server side sorting by shards and failed shards counts

* [Snapshot & Restore] Fixed i18n errors

* [Snapshot & Restore] Added server side sorting by repository

* [Snapshot & Restore] Implemented server side search request for snapshot, repository and policy name

* [Snapshot & Restore] Fixed eslint errors

* [Snapshot & Restore] Removed uncommented code

* [Snapshot & Restore] Fixed pagination/search bug

* [Snapshot & Restore] Fixed pagination/search bug

* [Snapshot & Restore] Fixed text truncate bug

* [Snapshot & Restore] Fixed non existent repository search error

* Update x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_search_bar.tsx

Co-authored-by: CJ Cenizal <cj@cenizal.com>

* Update x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_empty_prompt.tsx

Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com>

* [Snapshot & Restore] Fixed missing i18n and no snapshots callout

* [Snapshot & Restore] Moved "getSnapshotSearchWildcard" to a separate file and added unit tests

* [Snapshot & Restore] Added api integration tests for "get snapshots" endpoint (pagination, sorting, search)

* [Snapshot & Restore] Renamed SnapshotSearchOptions/SnapshotTableOptions to -Params and added the link to the specs issue

* [Snapshot & Restore] Fixed search wildcard to also match string in the middle of the value, not only starting with the string. Also updated the tests following the code review suggestions to make them more readable.

* [Snapshot & Restore] Added incremental search back to snapshots list and a debounce of 500ms

* [Snapshot & Restore] Updated snapshot search debounce value and extracted it into a constant

* [Snapshot & Restore] Renamed debounceValue to cachedListParams and added a comment why debounce is used

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: CJ Cenizal <cj@cenizal.com>
Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com>
  • Loading branch information
4 people authored Oct 18, 2021
1 parent 4dcb09d commit 83e9c7a
Show file tree
Hide file tree
Showing 28 changed files with 1,640 additions and 461 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> | any[];

Expand Down Expand Up @@ -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));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -431,6 +431,7 @@ describe('<SnapshotRestoreHome />', () => {
httpRequestsMockHelpers.setLoadSnapshotsResponse({
snapshots: [],
repositories: ['my-repo'],
total: 0,
});

testBed = await setup();
Expand Down Expand Up @@ -469,6 +470,7 @@ describe('<SnapshotRestoreHome />', () => {
httpRequestsMockHelpers.setLoadSnapshotsResponse({
snapshots,
repositories: [REPOSITORY_NAME],
total: 2,
});

testBed = await setup();
Expand Down Expand Up @@ -501,18 +503,10 @@ describe('<SnapshotRestoreHome />', () => {
});
});

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: {
Expand Down
6 changes: 0 additions & 6 deletions x-pack/plugins/snapshot_restore/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@
*/

export { useDecodedParams } from './use_decoded_params';

export {
SortField,
SortDirection,
SnapshotListParams,
getListParams,
getQueryFromListParams,
DEFAULT_SNAPSHOT_LIST_PARAMS,
} from './snapshot_list_params';
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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 (
<EuiPageContent
hasShadow={false}
paddingSize="none"
verticalPosition="center"
horizontalPosition="center"
data-test-subj="snapshotListEmpty"
>
<EuiEmptyPrompt
iconType="managementApp"
title={
<h1 data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noRepositoriesTitle"
defaultMessage="Start by registering a repository"
/>
</h1>
}
body={
<>
<p>
<FormattedMessage
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noRepositoriesDescription"
defaultMessage="You need a place where your snapshots will live."
/>
</p>
<p>
<EuiButton
{...reactRouterNavigate(history, linkToAddRepository())}
fill
iconType="plusInCircle"
data-test-subj="registerRepositoryButton"
>
<FormattedMessage
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noRepositoriesAddButtonLabel"
defaultMessage="Register a repository"
/>
</EuiButton>
</p>
</>
}
data-test-subj="emptyPrompt"
/>
</EuiPageContent>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="danger">
<EuiEmptyPrompt
iconType="managementApp"
data-test-subj="repositoryErrorsPrompt"
title={
<h1 data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.snapshotList.emptyPrompt.errorRepositoriesTitle"
defaultMessage="Some repositories contain errors"
/>
</h1>
}
body={
<p>
<FormattedMessage
id="xpack.snapshotRestore.snapshotList.emptyPrompt.repositoryWarningDescription"
defaultMessage="Go to {repositoryLink} to fix the errors."
values={{
repositoryLink: (
<EuiLink {...reactRouterNavigate(history, linkToRepositories())}>
<FormattedMessage
id="xpack.snapshotRestore.repositoryWarningLinkText"
defaultMessage="Repositories"
/>
</EuiLink>
),
}}
/>
</p>
}
/>
</EuiPageContent>
);
};
Loading

0 comments on commit 83e9c7a

Please sign in to comment.