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; +}