diff --git a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts
index a4c68dafa65ed..7b8b3798f347c 100644
--- a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts
+++ b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts
@@ -4,36 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export interface Snapshot {
- id: string;
- summary: SnapshotSummary;
- repositories: string[];
-}
-
-export interface SnapshotSummary {
- status: string;
- /** This and other numerical values are typed as strings. e.g. '1554501400'. */
- startEpoch: string;
- /** e.g. '21:56:40' */
- startTime: string;
- endEpoch: string;
- /** e.g. '21:56:45' */
- endTime: string;
- /** Includes unit, e.g. '4.7s' */
- duration: string;
- indices: string;
- successfulShards: string;
- failedShards: string;
- totalShards: string;
-}
-
export interface SnapshotDetails {
+ repository: string;
snapshot: string;
uuid: string;
versionId: number;
version: string;
indices: string[];
- includeGlobalState: boolean;
+ includeGlobalState: number;
state: string;
/** e.g. '2019-04-05T21:56:40.438Z' */
startTime: string;
diff --git a/x-pack/plugins/snapshot_restore/public/app/app.tsx b/x-pack/plugins/snapshot_restore/public/app/app.tsx
index b18f446e65f4f..507b8716fe27a 100644
--- a/x-pack/plugins/snapshot_restore/public/app/app.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/app.tsx
@@ -17,7 +17,11 @@ export const App = () => {
-
+
);
diff --git a/x-pack/plugins/snapshot_restore/public/app/components/repository_form/repository_form.tsx b/x-pack/plugins/snapshot_restore/public/app/components/repository_form/repository_form.tsx
index 83041356666f9..d3477176109a2 100644
--- a/x-pack/plugins/snapshot_restore/public/app/components/repository_form/repository_form.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/components/repository_form/repository_form.tsx
@@ -61,16 +61,8 @@ export const RepositoryForm: React.FunctionComponent = ({
error: repositoryTypesError,
loading: repositoryTypesLoading,
data: repositoryTypes,
- setIsMounted,
} = loadRepositoryTypes();
- // Set mounted to false when unmounting to avoid in-flight request setting state on unmounted component
- useEffect(() => {
- return () => {
- setIsMounted(false);
- };
- }, []);
-
// Repository state
const [repository, setRepository] = useState({
...originalRepository,
diff --git a/x-pack/plugins/snapshot_restore/public/app/index.tsx b/x-pack/plugins/snapshot_restore/public/app/index.tsx
index b94a1ca27e2b9..17e67fa5ca7e3 100644
--- a/x-pack/plugins/snapshot_restore/public/app/index.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/index.tsx
@@ -3,6 +3,7 @@
* 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, { createContext, useContext, useReducer } from 'react';
import { render } from 'react-dom';
import { HashRouter } from 'react-router-dom';
diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/home.tsx
index bd4ec07c96d91..f0a97a6a9e9b0 100644
--- a/x-pack/plugins/snapshot_restore/public/app/sections/home/home.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/home.tsx
@@ -99,8 +99,20 @@ export const SnapshotRestoreHome: React.FunctionComponent = ({
-
-
+
+ {/* We have two separate SnapshotList routes because repository names could have slashes in
+ * them. This would break a route with a path like snapshots/:repositoryName?/:snapshotId*
+ */}
+
+
diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx
index c1add77287ef5..7ac31a9916d17 100644
--- a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx
@@ -6,22 +6,6 @@
import React, { Fragment } from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
-import { useAppDependencies } from '../../../../index';
-import { documentationLinksService } from '../../../../services/documentation';
-import { loadRepository } from '../../../../services/http';
-import { textService } from '../../../../services/text';
-
-import { REPOSITORY_TYPES } from '../../../../../../common/constants';
-import { Repository } from '../../../../../../common/types';
-import {
- RepositoryDeleteProvider,
- RepositoryVerificationBadge,
- SectionError,
- SectionLoading,
-} from '../../../../components';
-import { BASE_PATH } from '../../../../constants';
-import { TypeDetails } from './type_details';
-
import {
EuiButton,
EuiButtonEmpty,
@@ -39,6 +23,22 @@ import {
import 'brace/theme/textmate';
+import { useAppDependencies } from '../../../../index';
+import { documentationLinksService } from '../../../../services/documentation';
+import { loadRepository } from '../../../../services/http';
+import { textService } from '../../../../services/text';
+
+import { REPOSITORY_TYPES } from '../../../../../../common/constants';
+import { Repository } from '../../../../../../common/types';
+import {
+ RepositoryDeleteProvider,
+ RepositoryVerificationBadge,
+ SectionError,
+ SectionLoading,
+} from '../../../../components';
+import { BASE_PATH } from '../../../../constants';
+import { TypeDetails } from './type_details';
+
interface Props extends RouteComponentProps {
repositoryName: Repository['name'];
onClose: () => void;
@@ -77,7 +77,7 @@ const RepositoryDetailsUi: React.FunctionComponent = ({
);
diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx
index f5baeeb6868ae..c0e8f7d9d860e 100644
--- a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx
@@ -19,13 +19,13 @@ import { RepositoryTable } from './repository_table';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
interface MatchParams {
- name?: Repository['name'];
+ repositoryName?: Repository['name'];
}
interface Props extends RouteComponentProps {}
export const RepositoryList: React.FunctionComponent = ({
match: {
- params: { name },
+ params: { repositoryName: name },
},
history,
}) => {
@@ -64,7 +64,7 @@ export const RepositoryList: React.FunctionComponent = ({
);
diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/index.ts b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/index.ts
new file mode 100644
index 0000000000000..a4934bae7dfa7
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { SnapshotDetails } from './snapshot_details';
diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx
new file mode 100644
index 0000000000000..2bdee5d519eba
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx
@@ -0,0 +1,359 @@
+/*
+ * 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 {
+ EuiDescriptionList,
+ EuiDescriptionListDescription,
+ EuiDescriptionListTitle,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiText,
+ EuiTextColor,
+ EuiTitle,
+} from '@elastic/eui';
+import React from 'react';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+import { SectionError, SectionLoading } from '../../../../components';
+import { useAppDependencies } from '../../../../index';
+import { loadSnapshot } from '../../../../services/http';
+import { formatDate } from '../../../../services/text';
+
+interface Props extends RouteComponentProps {
+ repositoryName: string;
+ snapshotId: string;
+ onClose: () => void;
+}
+
+const SnapshotDetailsUi: React.FunctionComponent = ({
+ repositoryName,
+ snapshotId,
+ onClose,
+}) => {
+ const {
+ core: {
+ i18n: { FormattedMessage, translate },
+ },
+ } = useAppDependencies();
+
+ const { error, data: snapshotDetails } = loadSnapshot(repositoryName, snapshotId);
+
+ const includeGlobalStateToHumanizedMap: Record = {
+ 0: (
+
+ ),
+ 1: (
+
+ ),
+ };
+
+ let content;
+
+ if (snapshotDetails) {
+ const {
+ versionId,
+ version,
+ // TODO: Add a tooltip explaining that: a false value means that the cluster global state
+ // is not stored as part of the snapshot.
+ includeGlobalState,
+ indices,
+ state,
+ failures,
+ startTimeInMillis,
+ endTimeInMillis,
+ durationInMillis,
+ uuid,
+ } = snapshotDetails;
+
+ const indicesList = indices.length ? (
+
+ {indices.map((index: string) => (
+ -
+
+ {index}
+
+
+ ))}
+
+ ) : (
+
+ );
+
+ const failuresList = failures.length ? (
+
+ {failures.map((failure: any) => (
+ -
+
+ {JSON.stringify(failure)}
+
+
+ ))}
+
+ ) : (
+
+ );
+
+ content = (
+
+
+
+
+
+
+
+
+ {version} / {versionId}
+
+
+
+
+
+
+
+
+
+ {uuid}
+
+
+
+
+
+
+
+
+
+
+
+ {includeGlobalStateToHumanizedMap[includeGlobalState]}
+
+
+
+
+
+
+
+
+
+ {state}
+
+
+
+
+
+
+
+
+
+
+
+ {indicesList}
+
+
+
+
+
+
+
+
+
+ {failuresList}
+
+
+
+
+
+
+
+
+
+
+
+ {formatDate(startTimeInMillis)}
+
+
+
+
+
+
+
+
+
+ {formatDate(endTimeInMillis)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ } else if (error) {
+ const notFound = error.status === 404;
+ const errorObject = notFound
+ ? {
+ data: {
+ error: translate('xpack.snapshotRestore.snapshotDetails.errorSnapshotNotFound', {
+ defaultMessage: `Either the snapshot '{snapshotId}' doesn't exist in the repository '{repositoryName}' or that repository doesn't exist.`,
+ values: {
+ snapshotId,
+ repositoryName,
+ },
+ }),
+ },
+ }
+ : error;
+ content = (
+
+ }
+ error={errorObject}
+ />
+ );
+ } else {
+ // Assume the content is loading.
+ content = (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {snapshotId}
+
+
+
+
+
+
+
+ {repositoryName}
+
+
+
+
+
+
+ {content}
+
+ );
+};
+
+export const SnapshotDetails = withRouter(SnapshotDetailsUi);
diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
index a45ef4fbb27f6..238a6ec77f072 100644
--- a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
+++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx
@@ -4,8 +4,135 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { Fragment } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
-export const SnapshotList: React.FunctionComponent = () => {
- return List of snapshots
;
+import { SectionError, SectionLoading } from '../../../components';
+import { BASE_PATH } from '../../../constants';
+import { useAppDependencies } from '../../../index';
+import { documentationLinksService } from '../../../services/documentation';
+import { loadSnapshots } from '../../../services/http';
+
+import { SnapshotDetails } from './snapshot_details';
+import { SnapshotTable } from './snapshot_table';
+
+import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
+
+interface MatchParams {
+ repositoryName?: string;
+ snapshotId?: string;
+}
+
+interface Props extends RouteComponentProps {}
+
+export const SnapshotList: React.FunctionComponent = ({
+ match: {
+ params: { repositoryName, snapshotId },
+ },
+ history,
+}) => {
+ const {
+ core: {
+ i18n: { FormattedMessage },
+ },
+ } = useAppDependencies();
+
+ const {
+ error,
+ loading,
+ data: { snapshots },
+ request: reload,
+ } = loadSnapshots();
+
+ const openSnapshotDetails = (repositoryNameToOpen: string, snapshotIdToOpen: string) => {
+ history.push(`${BASE_PATH}/snapshots/${repositoryNameToOpen}/${snapshotIdToOpen}`);
+ };
+
+ const closeSnapshotDetails = () => {
+ history.push(`${BASE_PATH}/snapshots`);
+ };
+
+ let content;
+
+ if (loading) {
+ content = (
+
+
+
+ );
+ } else if (error) {
+ content = (
+
+ }
+ error={error}
+ />
+ );
+ } else if (snapshots && snapshots.length === 0) {
+ content = (
+
+
+
+ }
+ body={
+
+
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+ );
+ } else {
+ content = (
+
+ );
+ }
+
+ return (
+
+ {repositoryName && snapshotId ? (
+
+ ) : null}
+ {content}
+
+ );
};
diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/index.ts b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/index.ts
new file mode 100644
index 0000000000000..f09487fd3f10a
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { SnapshotTable } from './snapshot_table';
diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx
new file mode 100644
index 0000000000000..e7fded3807260
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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 { RouteComponentProps, withRouter } from 'react-router-dom';
+
+import { EuiButton, EuiInMemoryTable, EuiLink } from '@elastic/eui';
+
+import { SnapshotDetails } from '../../../../../../common/types';
+import { useAppDependencies } from '../../../../index';
+import { formatDate } from '../../../../services/text';
+
+interface Props extends RouteComponentProps {
+ snapshots: SnapshotDetails[];
+ reload: () => Promise;
+ openSnapshotDetails: (repositoryName: string, snapshotId: string) => void;
+}
+
+const SnapshotTableUi: React.FunctionComponent = ({
+ snapshots,
+ reload,
+ openSnapshotDetails,
+ history,
+}) => {
+ const {
+ core: {
+ i18n: { FormattedMessage, translate },
+ },
+ } = useAppDependencies();
+
+ const columns = [
+ {
+ field: 'snapshot',
+ name: translate('xpack.snapshotRestore.snapshotList.table.snapshotColumnTitle', {
+ defaultMessage: 'Snapshot',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (snapshotId: string, snapshot: SnapshotDetails) => (
+ openSnapshotDetails(snapshot.repository, snapshotId)}>
+ {snapshotId}
+
+ ),
+ },
+ {
+ field: 'repository',
+ name: translate('xpack.snapshotRestore.snapshotList.table.repositoryColumnTitle', {
+ defaultMessage: 'Repository',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (repository: string) => repository,
+ },
+ {
+ field: 'startTimeInMillis',
+ name: translate('xpack.snapshotRestore.snapshotList.table.startTimeColumnTitle', {
+ defaultMessage: 'Date created',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (startTimeInMillis: number) => formatDate(startTimeInMillis),
+ },
+ {
+ field: 'durationInMillis',
+ name: translate('xpack.snapshotRestore.snapshotList.table.durationColumnTitle', {
+ defaultMessage: 'Duration',
+ }),
+ truncateText: true,
+ sortable: true,
+ width: '100px',
+ render: (durationInMillis: number) => (
+
+ ),
+ },
+ {
+ field: 'indices',
+ name: translate('xpack.snapshotRestore.snapshotList.table.indicesColumnTitle', {
+ defaultMessage: 'Indices',
+ }),
+ truncateText: true,
+ sortable: true,
+ width: '100px',
+ render: (indices: string[]) => indices.length,
+ },
+ {
+ field: 'shards.total',
+ name: translate('xpack.snapshotRestore.snapshotList.table.shardsColumnTitle', {
+ defaultMessage: 'Shards',
+ }),
+ truncateText: true,
+ sortable: true,
+ width: '100px',
+ render: (totalShards: number) => totalShards,
+ },
+ {
+ field: 'shards.failed',
+ name: translate('xpack.snapshotRestore.snapshotList.table.failedShardsColumnTitle', {
+ defaultMessage: 'Failed shards',
+ }),
+ truncateText: true,
+ sortable: true,
+ width: '100px',
+ render: (failedShards: number) => failedShards,
+ },
+ ];
+
+ // By default, we'll display the most recent snapshots at the top of the table.
+ const sorting = {
+ sort: {
+ field: 'startTimeInMillis',
+ direction: 'desc',
+ },
+ };
+
+ const pagination = {
+ initialPageSize: 20,
+ pageSizeOptions: [10, 20, 50],
+ };
+
+ const search = {
+ toolsRight: (
+
+
+
+ ),
+ box: {
+ incremental: true,
+ schema: true,
+ },
+ };
+
+ return (
+ ({
+ 'data-test-subj': 'srSnapshotListTableRow',
+ })}
+ cellProps={(item: any, column: any) => ({
+ 'data-test-subj': `srSnapshotListTableCell-${column.field}`,
+ })}
+ />
+ );
+};
+
+export const SnapshotTable = withRouter(SnapshotTableUi);
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/documentation/documentation_links.ts b/x-pack/plugins/snapshot_restore/public/app/services/documentation/documentation_links.ts
index 1fcb3fd7c6e4f..144d41c4c1fe6 100644
--- a/x-pack/plugins/snapshot_restore/public/app/services/documentation/documentation_links.ts
+++ b/x-pack/plugins/snapshot_restore/public/app/services/documentation/documentation_links.ts
@@ -40,6 +40,10 @@ class DocumentationLinksService {
return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.default}`;
}
}
+
+ public getSnapshotDocUrl() {
+ return `${this.esDocBasePath}/modules-snapshots.html#_snapshot`;
+ }
}
export const documentationLinksService = new DocumentationLinksService();
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/http/index.ts b/x-pack/plugins/snapshot_restore/public/app/services/http/index.ts
index 87f938c296181..d8eda129b6966 100644
--- a/x-pack/plugins/snapshot_restore/public/app/services/http/index.ts
+++ b/x-pack/plugins/snapshot_restore/public/app/services/http/index.ts
@@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { httpService } from './http';
-export * from './requests';
+export * from './repository_requests';
+export * from './snapshot_requests';
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/http/requests.ts b/x-pack/plugins/snapshot_restore/public/app/services/http/repository_requests.ts
similarity index 96%
rename from x-pack/plugins/snapshot_restore/public/app/services/http/requests.ts
rename to x-pack/plugins/snapshot_restore/public/app/services/http/repository_requests.ts
index 9822d2211ae0b..3538ef1c66336 100644
--- a/x-pack/plugins/snapshot_restore/public/app/services/http/requests.ts
+++ b/x-pack/plugins/snapshot_restore/public/app/services/http/repository_requests.ts
@@ -12,6 +12,7 @@ export const loadRepositories = () => {
return useRequest({
path: httpService.addBasePath(`${API_BASE_PATH}repositories`),
method: 'get',
+ initialData: [],
});
};
@@ -19,6 +20,7 @@ export const loadRepository = (name: Repository['name']) => {
return useRequest({
path: httpService.addBasePath(`${API_BASE_PATH}repositories/${encodeURIComponent(name)}`),
method: 'get',
+ initialData: {},
});
};
@@ -26,6 +28,7 @@ export const loadRepositoryTypes = () => {
return useRequest({
path: httpService.addBasePath(`${API_BASE_PATH}repository_types`),
method: 'get',
+ initialData: [],
});
};
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/http/snapshot_requests.ts b/x-pack/plugins/snapshot_restore/public/app/services/http/snapshot_requests.ts
new file mode 100644
index 0000000000000..050fd1adc3fb8
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/app/services/http/snapshot_requests.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { API_BASE_PATH } from '../../../../common/constants';
+import { httpService } from './http';
+import { useRequest } from './use_request';
+
+export const loadSnapshots = () =>
+ useRequest({
+ path: httpService.addBasePath(`${API_BASE_PATH}snapshots`),
+ method: 'get',
+ initialData: [],
+ });
+
+export const loadSnapshot = (repositoryName: string, snapshotId: string) =>
+ useRequest({
+ path: httpService.addBasePath(
+ `${API_BASE_PATH}snapshots/${encodeURIComponent(repositoryName)}/${encodeURIComponent(
+ snapshotId
+ )}`
+ ),
+ method: 'get',
+ });
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/http/use_request.ts b/x-pack/plugins/snapshot_restore/public/app/services/http/use_request.ts
index c1c85dbb7a342..a90370d6c9b55 100644
--- a/x-pack/plugins/snapshot_restore/public/app/services/http/use_request.ts
+++ b/x-pack/plugins/snapshot_restore/public/app/services/http/use_request.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useState } from 'react';
import { httpService } from './index';
interface SendRequest {
@@ -41,44 +41,56 @@ export const sendRequest = async ({
interface UseRequest extends SendRequest {
interval?: number;
+ initialData?: any;
}
-export const useRequest = ({ path, method, body, interval }: UseRequest) => {
+export const useRequest = ({ path, method, body, interval, initialData }: UseRequest) => {
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
- const [data, setData] = useState([]);
- const isMounted = useRef(true);
+ const [data, setData] = useState(initialData);
+
+ // Tied to every render and bound to each request.
+ let isOutdatedRequest = false;
const request = async () => {
setError(null);
+ setData(initialData);
setLoading(true);
+
const { data: responseData, error: responseError } = await sendRequest({
path,
method,
body,
});
- // Avoid setting state for components that unmounted before request completed
- if (!isMounted.current) {
+
+ // Don't update state if an outdated request has resolved.
+ if (isOutdatedRequest) {
return;
}
- // Set state for components that are still mounted
- if (responseError) {
- setError(responseError);
- } else {
- setData(responseData);
- }
+
+ setError(responseError);
+ setData(responseData);
setLoading(false);
};
useEffect(
() => {
+ function cancelOutdatedRequest() {
+ isOutdatedRequest = true;
+ }
+
request();
+
if (interval) {
const intervalRequest = setInterval(request, interval);
return () => {
+ cancelOutdatedRequest();
clearInterval(intervalRequest);
};
}
+
+ // Called when a new render will trigger this effect.
+ return cancelOutdatedRequest;
},
[path]
);
@@ -88,8 +100,5 @@ export const useRequest = ({ path, method, body, interval }: UseRequest) => {
loading,
data,
request,
- setIsMounted: (status: boolean) => {
- isMounted.current = status;
- },
};
};
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/text/format_date.ts b/x-pack/plugins/snapshot_restore/public/app/services/text/format_date.ts
new file mode 100644
index 0000000000000..26b0f85beffb7
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/app/services/text/format_date.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { dateFormatAliases } from '@elastic/eui/lib/services/format';
+import moment from 'moment';
+
+export function formatDate(epochMs: number): string {
+ return moment(Number(epochMs)).format(dateFormatAliases.longDateTime);
+}
diff --git a/x-pack/plugins/snapshot_restore/public/app/services/text/index.ts b/x-pack/plugins/snapshot_restore/public/app/services/text/index.ts
index 3bed86f69937a..f442bfbc0ab23 100644
--- a/x-pack/plugins/snapshot_restore/public/app/services/text/index.ts
+++ b/x-pack/plugins/snapshot_restore/public/app/services/text/index.ts
@@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export { formatDate } from './format_date';
export { textService } from './text';
diff --git a/x-pack/plugins/snapshot_restore/server/lib/index.ts b/x-pack/plugins/snapshot_restore/server/lib/index.ts
index 27673530f4b9f..0de34961db2f6 100644
--- a/x-pack/plugins/snapshot_restore/server/lib/index.ts
+++ b/x-pack/plugins/snapshot_restore/server/lib/index.ts
@@ -5,4 +5,4 @@
*/
export { booleanizeSettings } from './booleanize_settings';
-export { deserializeSnapshotSummary, deserializeSnapshotDetails } from './snapshot_serialization';
+export { deserializeSnapshotDetails } from './snapshot_serialization';
diff --git a/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.ts b/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.ts
index d7ee0be1dd00f..c623d062400db 100644
--- a/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.ts
+++ b/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.ts
@@ -4,42 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SnapshotDetails, SnapshotSummary } from '../../common/types';
-import { SnapshotDetailsEs, SnapshotSummaryEs } from '../types';
+import { SnapshotDetails } from '../../common/types';
+import { SnapshotDetailsEs } from '../types';
-export function deserializeSnapshotSummary(snapshotSummaryEs: SnapshotSummaryEs): SnapshotSummary {
- if (!snapshotSummaryEs || typeof snapshotSummaryEs !== 'object') {
- throw new Error('Unable to deserialize snapshot summary');
- }
-
- const {
- status,
- start_epoch: startEpoch,
- start_time: startTime,
- end_epoch: endEpoch,
- end_time: endTime,
- duration,
- indices,
- successful_shards: successfulShards,
- failed_shards: failedShards,
- total_shards: totalShards,
- } = snapshotSummaryEs;
-
- return {
- status,
- startEpoch,
- startTime,
- endEpoch,
- endTime,
- duration,
- indices,
- successfulShards,
- failedShards,
- totalShards,
- };
-}
-
-export function deserializeSnapshotDetails(snapshotDetailsEs: SnapshotDetailsEs): SnapshotDetails {
+export function deserializeSnapshotDetails(
+ repository: string,
+ snapshotDetailsEs: SnapshotDetailsEs
+): SnapshotDetails {
if (!snapshotDetailsEs || typeof snapshotDetailsEs !== 'object') {
throw new Error('Unable to deserialize snapshot details');
}
@@ -62,12 +33,13 @@ export function deserializeSnapshotDetails(snapshotDetailsEs: SnapshotDetailsEs)
} = snapshotDetailsEs;
return {
+ repository,
snapshot,
uuid,
versionId,
version,
indices,
- includeGlobalState,
+ includeGlobalState: Boolean(includeGlobalState) ? 1 : 0,
state,
startTime,
startTimeInMillis,
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 3cea3e57ad60d..c4a2bc6f86134 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
@@ -7,6 +7,24 @@
import { Request, ResponseToolkit } from 'hapi';
import { getAllHandler, getOneHandler } from './snapshots';
+const defaultSnapshot = {
+ repository: undefined,
+ snapshot: undefined,
+ uuid: undefined,
+ versionId: undefined,
+ version: undefined,
+ indices: undefined,
+ includeGlobalState: 0,
+ state: undefined,
+ startTime: undefined,
+ startTimeInMillis: undefined,
+ endTime: undefined,
+ endTimeInMillis: undefined,
+ durationInMillis: undefined,
+ failures: undefined,
+ shards: undefined,
+};
+
describe('[Snapshot and Restore API Routes] Snapshots', () => {
const mockResponseToolkit = {} as ResponseToolkit;
@@ -19,29 +37,33 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
barRepository: {},
};
- const mockCatSnapshotsFooResponse = Promise.resolve([
- {
- id: 'snapshot1',
- },
- ]);
+ const mockGetSnapshotsFooResponse = Promise.resolve({
+ snapshots: [
+ {
+ snapshot: 'snapshot1',
+ },
+ ],
+ });
- const mockCatSnapshotsBarResponse = Promise.resolve([
- {
- id: 'snapshot2',
- },
- ]);
+ const mockGetSnapshotsBarResponse = Promise.resolve({
+ snapshots: [
+ {
+ snapshot: 'snapshot2',
+ },
+ ],
+ });
const callWithRequest = jest
.fn()
.mockReturnValueOnce(mockSnapshotGetRepositoryEsResponse)
- .mockReturnValueOnce(mockCatSnapshotsFooResponse)
- .mockReturnValueOnce(mockCatSnapshotsBarResponse);
+ .mockReturnValueOnce(mockGetSnapshotsFooResponse)
+ .mockReturnValueOnce(mockGetSnapshotsBarResponse);
const expectedResponse = {
errors: [],
snapshots: [
- { repositories: ['fooRepository'], id: 'snapshot1', summary: {} },
- { repositories: ['barRepository'], id: 'snapshot2', summary: {} },
+ { ...defaultSnapshot, repository: 'fooRepository', snapshot: 'snapshot1' },
+ { ...defaultSnapshot, repository: 'barRepository', snapshot: 'snapshot2' },
],
};
@@ -85,7 +107,11 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
snapshots: [{ snapshot }],
};
const callWithRequest = jest.fn().mockReturnValue(mockSnapshotGetEsResponse);
- const expectedResponse = { snapshot };
+ const expectedResponse = {
+ ...defaultSnapshot,
+ snapshot,
+ repository,
+ };
const response = await getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit);
expect(response).toEqual(expectedResponse);
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 58b7d745f80a7..6cac8134d32dd 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router';
-import { Snapshot, SnapshotDetails } from '../../../common/types';
-import { deserializeSnapshotDetails, deserializeSnapshotSummary } from '../../lib';
-import { SnapshotDetailsEs, SnapshotSummaryEs } from '../../types';
+import { SnapshotDetails } from '../../../common/types';
+import { deserializeSnapshotDetails } from '../../lib';
+import { SnapshotDetailsEs } from '../../types';
export function registerSnapshotsRoutes(router: Router) {
router.get('snapshots', getAllHandler);
@@ -17,7 +17,7 @@ export const getAllHandler: RouterRouteHandler = async (
req,
callWithRequest
): Promise<{
- snapshots: Snapshot[];
+ snapshots: SnapshotDetails[];
errors: any[];
}> => {
const repositoriesByName = await callWithRequest('snapshot.getRepository', {
@@ -30,59 +30,35 @@ export const getAllHandler: RouterRouteHandler = async (
return { snapshots: [], errors: [] };
}
+ const snapshots: SnapshotDetails[] = [];
const errors: any = [];
- const fetchSnapshotsForRepository = async (repositoryName: string) => {
+ const fetchSnapshotsForRepository = async (repository: string) => {
try {
- const snapshots = await callWithRequest('cat.snapshots', {
- repository: repositoryName,
- format: 'json',
+ // If any of these repositories 504 they will cost the request significant time.
+ const {
+ snapshots: fetchedSnapshots,
+ }: { snapshots: SnapshotDetailsEs[] } = await callWithRequest('snapshot.get', {
+ repository,
+ snapshot: '_all',
+ ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable.
});
// Decorate each snapshot with the repository with which it's associated.
- return snapshots.map((snapshot: any) => ({
- repository: repositoryName,
- ...snapshot,
- }));
+ fetchedSnapshots.forEach((snapshot: SnapshotDetailsEs) => {
+ snapshots.push(deserializeSnapshotDetails(repository, snapshot));
+ });
} catch (error) {
// These errors are commonly due to a misconfiguration in the repository or plugin errors,
// which can result in a variety of 400, 404, and 500 errors.
errors.push(error);
- return null;
}
};
- const repositoriesSnapshots = await Promise.all(repositoryNames.map(fetchSnapshotsForRepository));
-
- // Multiple repositories can have identical configurations. This means that the same snapshot
- // may be listed as belonging to multiple repositories. A map lets us dedupe the snapshots and
- // aggregate the repositories that are associated with each one.
- const idToSnapshotMap: Record = repositoriesSnapshots
- .filter(Boolean)
- .reduce((idToSnapshot, snapshots: SnapshotSummaryEs[]) => {
- // create an object to store each snapshot and the
- // repositories that are associated with it.
- snapshots.forEach(summary => {
- const { id, repository } = summary;
-
- if (!idToSnapshot[id]) {
- // Instantiate the snapshot object
- idToSnapshot[id] = {
- id,
- // The cat API only returns a subset of the details returned by the get snapshot API.
- summary: deserializeSnapshotSummary(summary),
- repositories: [],
- };
- }
-
- idToSnapshot[id].repositories.push(repository);
- });
-
- return idToSnapshot;
- }, {});
+ await Promise.all(repositoryNames.map(fetchSnapshotsForRepository));
return {
- snapshots: Object.values(idToSnapshotMap),
+ snapshots,
errors,
};
};
@@ -98,5 +74,5 @@ export const getOneHandler: RouterRouteHandler = async (
});
// If the snapshot is missing the endpoint will return a 404, so we'll never get to this point.
- return deserializeSnapshotDetails(snapshots[0]);
+ return deserializeSnapshotDetails(repository, snapshots[0]);
};
diff --git a/x-pack/plugins/snapshot_restore/server/types/snapshot.ts b/x-pack/plugins/snapshot_restore/server/types/snapshot.ts
index 3231eec099330..280fcc56ba559 100644
--- a/x-pack/plugins/snapshot_restore/server/types/snapshot.ts
+++ b/x-pack/plugins/snapshot_restore/server/types/snapshot.ts
@@ -4,25 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export interface SnapshotSummaryEs {
- id: string;
- repository: string;
- status: string;
- /** This and other numerical values are typed as strings. e.g. '1554501400'. */
- start_epoch: string;
- /** e.g. '21:56:40' */
- start_time: string;
- end_epoch: string;
- /** e.g. '21:56:45' */
- end_time: string;
- /** Includes unit, e.g. '4.7s' */
- duration: string;
- indices: string;
- successful_shards: string;
- failed_shards: string;
- total_shards: string;
-}
-
export interface SnapshotDetailsEs {
snapshot: string;
uuid: string;
diff --git a/x-pack/typings/@elastic/eui/index.d.ts b/x-pack/typings/@elastic/eui/index.d.ts
index 537e636ceb110..afdda6f842239 100644
--- a/x-pack/typings/@elastic/eui/index.d.ts
+++ b/x-pack/typings/@elastic/eui/index.d.ts
@@ -10,3 +10,7 @@ declare module '@elastic/eui' {
export const EuiDescribedFormGroup: React.SFC;
export const EuiCodeEditor: React.SFC;
}
+
+declare module '@elastic/eui/lib/services/format' {
+ export const dateFormatAliases: any;
+}