diff --git a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts index 7b8b3798f347c..5e2016601b42a 100644 --- a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts @@ -20,7 +20,7 @@ export interface SnapshotDetails { endTime: string; endTimeInMillis: number; durationInMillis: number; - failures: string[]; + indexFailures: any[]; shards: SnapshotDetailsShardsStatus; } diff --git a/x-pack/plugins/snapshot_restore/public/app/components/data_placeholder.tsx b/x-pack/plugins/snapshot_restore/public/app/components/data_placeholder.tsx new file mode 100644 index 0000000000000..5908a34a67d73 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/components/data_placeholder.tsx @@ -0,0 +1,28 @@ +/* + * 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 { useAppDependencies } from '../index'; + +interface Props { + data: any; + children: React.ReactNode; +} + +export const DataPlaceholder: React.SFC = ({ data, children }) => { + const { + core: { i18n }, + } = useAppDependencies(); + + if (data != null) { + return children; + } + + return i18n.translate('xpack.snapshotRestore.dataPlaceholderLabel', { + defaultMessage: '-', + }); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/components/index.ts b/x-pack/plugins/snapshot_restore/public/app/components/index.ts index 21fcdf99af490..1cb77c4007a52 100644 --- a/x-pack/plugins/snapshot_restore/public/app/components/index.ts +++ b/x-pack/plugins/snapshot_restore/public/app/components/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +export { DataPlaceholder } from './data_placeholder'; export { RepositoryDeleteProvider } from './repository_delete_provider'; -export { RepositoryVerificationBadge } from './repository_verification_badge'; export { RepositoryForm } from './repository_form'; - +export { RepositoryVerificationBadge } from './repository_verification_badge'; export { SectionError } from './section_error'; export { SectionLoading } from './section_loading'; diff --git a/x-pack/plugins/snapshot_restore/public/app/constants/index.ts b/x-pack/plugins/snapshot_restore/public/app/constants/index.ts index bc237a5771585..18cc1c0af9164 100644 --- a/x-pack/plugins/snapshot_restore/public/app/constants/index.ts +++ b/x-pack/plugins/snapshot_restore/public/app/constants/index.ts @@ -18,3 +18,11 @@ export enum REPOSITORY_DOC_PATHS { gcs = 'repository-gcs.html', plugins = 'repository.html', } + +export enum SNAPSHOT_STATE { + IN_PROGRESS = 'IN_PROGRESS', + SUCCESS = 'SUCCESS', + FAILED = 'FAILED', + PARTIAL = 'PARTIAL', + INCOMPATIBLE = 'INCOMPATIBLE', +} 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 6aadc32136a79..5722ecbd96e5d 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 @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; import { BASE_PATH, Section } from '../../constants'; import { useAppDependencies } from '../../index'; -import { breadcrumbService } from '../../services/breadcrumb'; +import { breadcrumbService } from '../../services/navigation'; import { RepositoryList } from './repository_list'; import { SnapshotList } from './snapshot_list'; @@ -26,8 +26,6 @@ export const SnapshotRestoreHome: React.FunctionComponent { - const [activeSection, setActiveSection] = useState
(section); - const { core: { i18n: { FormattedMessage }, @@ -58,7 +56,6 @@ export const SnapshotRestoreHome: React.FunctionComponent { - setActiveSection(newSection); history.push(`${BASE_PATH}/${newSection}`); }; @@ -85,7 +82,7 @@ export const SnapshotRestoreHome: React.FunctionComponent ( onSectionChange(tab.id)} - isSelected={tab.id === activeSection} + isSelected={tab.id === section} key={tab.id} data-test-subject={tab.testSubj} > 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 index 2bdee5d519eba..26675cad0244a 100644 --- 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 @@ -5,24 +5,26 @@ */ import { - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, + EuiLink, + EuiSpacer, + EuiTab, + EuiTabs, EuiText, - EuiTextColor, EuiTitle, } from '@elastic/eui'; -import React from 'react'; +import React, { Fragment, useState, useEffect } 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'; +import { linkToRepository } from '../../../../services/navigation'; +import { TabSummary, TabFailures } from './tabs'; interface Props extends RouteComponentProps { repositoryName: string; @@ -30,6 +32,9 @@ interface Props extends RouteComponentProps { onClose: () => void; } +const TAB_SUMMARY = 'summary'; +const TAB_FAILURES = 'failures'; + const SnapshotDetailsUi: React.FunctionComponent = ({ repositoryName, snapshotId, @@ -43,248 +48,69 @@ const SnapshotDetailsUi: React.FunctionComponent = ({ const { error, data: snapshotDetails } = loadSnapshot(repositoryName, snapshotId); - const includeGlobalStateToHumanizedMap: Record = { - 0: ( - - ), - 1: ( - - ), - }; + const [activeTab, setActiveTab] = useState(TAB_SUMMARY); - 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)} - - - - - - - + // Reset tab when we look at a different snapshot. + useEffect( + () => { + setActiveTab(TAB_SUMMARY); + }, + [repositoryName, snapshotId] + ); - - {formatDate(endTimeInMillis)} - - - + let tabs; - - - - - + let content; - + ), + testSubj: 'srSnapshotDetailsSummaryTab', + }, + { + id: TAB_FAILURES, + name: ( + + ), + testSubj: 'srSnapshotDetailsFailuresTab', + }, + ]; + + tabs = ( + + + + {tabOptions.map(tab => ( + setActiveTab(tab.id)} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subject={tab.testSubj} > - - - - - + {tab.name} +
+ ))} + + ); + + if (activeTab === TAB_SUMMARY) { + content = ; + } else if (activeTab === TAB_FAILURES) { + content = ; + } } else if (error) { const notFound = error.status === 404; const errorObject = notFound @@ -300,11 +126,12 @@ const SnapshotDetailsUi: React.FunctionComponent = ({ }, } : error; + content = ( } @@ -342,13 +169,21 @@ const SnapshotDetailsUi: React.FunctionComponent = ({ - +

- {repositoryName} + + +

-
+
+ + {tabs} {content} diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/index.ts b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/index.ts new file mode 100644 index 0000000000000..e39895c5980d0 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TabSummary } from './tab_summary'; +export { TabFailures } from './tab_failures'; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx new file mode 100644 index 0000000000000..c14c4bf39347b --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiLoadingSpinner } from '@elastic/eui'; + +import { SNAPSHOT_STATE } from '../../../../../constants'; +import { useAppDependencies } from '../../../../../index'; + +interface Props { + state: any; +} + +export const SnapshotState: React.SFC = ({ state }) => { + const { + core: { + i18n: { translate }, + }, + } = useAppDependencies(); + + const stateMap: any = { + [SNAPSHOT_STATE.IN_PROGRESS]: { + icon: , + label: translate('xpack.snapshotRestore.snapshotState.inProgressLabel', { + defaultMessage: 'Taking snapshot…', + }), + }, + [SNAPSHOT_STATE.SUCCESS]: { + icon: , + label: translate('xpack.snapshotRestore.snapshotState.inProgressLabel', { + defaultMessage: 'Snapshot complete', + }), + }, + [SNAPSHOT_STATE.FAILED]: { + icon: , + label: translate('xpack.snapshotRestore.snapshotState.failedLabel', { + defaultMessage: 'Snapshot failed', + }), + }, + [SNAPSHOT_STATE.PARTIAL]: { + icon: , + label: translate('xpack.snapshotRestore.snapshotState.partialLabel', { + defaultMessage: 'Partial failure', + }), + tip: translate('xpack.snapshotRestore.snapshotState.partialTipDescription', { + defaultMessage: `Global cluster state was stored, but at least one shard wasn't stored successfully. See the 'Failed indices' tab.`, + }), + }, + [SNAPSHOT_STATE.INCOMPATIBLE]: { + icon: , + label: translate('xpack.snapshotRestore.snapshotState.incompatibleLabel', { + defaultMessage: 'Incompatible version', + }), + tip: translate('xpack.snapshotRestore.snapshotState.partialTipDescription', { + defaultMessage: `Snapshot was created with a version of Elasticsearch incompatible with the cluster's version.`, + }), + }, + }; + + if (!stateMap[state]) { + // Help debug unexpected state. + return state; + } + + const { icon, label, tip } = stateMap[state]; + + const iconTip = tip && ( + + {' '} + + + ); + + return ( + + {icon} + + + {/* Escape flex layout created by EuiFlexItem. */} +
+ {label} + {iconTip} +
+
+
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx new file mode 100644 index 0000000000000..2df923ff2ba8e --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx @@ -0,0 +1,84 @@ +/* + * 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 { EuiCodeBlock, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; + +import { SNAPSHOT_STATE } from '../../../../../constants'; +import { useAppDependencies } from '../../../../../index'; + +interface Props { + indexFailures: any; + snapshotState: string; +} + +export const TabFailures: React.SFC = ({ indexFailures, snapshotState }) => { + const { + core: { + i18n: { FormattedMessage }, + }, + } = useAppDependencies(); + + if (!indexFailures.length) { + // If the snapshot is in progress then we still might encounter errors later. + if (snapshotState === SNAPSHOT_STATE.IN_PROGRESS) { + return ( + + ); + } else { + return ( + + ); + } + } + + return indexFailures.map((indexObject: any, count: number) => { + const { index, failures } = indexObject; + + return ( +
+ +

{index}

+
+ + + + {failures.map((failure: any, failuresCount: number) => { + const { status, reason, shard_id: shardId } = failure; + + return ( +
+ +

+ +

+
+ + + {status}: {reason} + + + {failuresCount < failures.length - 1 ? : undefined} +
+ ); + })} + + {count < indexFailures.length - 1 ? : undefined} +
+ ); + }); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx new file mode 100644 index 0000000000000..462df1ebc6bca --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx @@ -0,0 +1,241 @@ +/* + * 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 { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { useAppDependencies } from '../../../../../index'; +import { formatDate } from '../../../../../services/text'; +import { DataPlaceholder } from '../../../../../components'; +import { SnapshotState } from './snapshot_state'; + +interface Props { + snapshotDetails: any; +} + +export const TabSummary: React.SFC = ({ snapshotDetails }) => { + const { + core: { + i18n: { FormattedMessage }, + }, + } = useAppDependencies(); + + const includeGlobalStateToHumanizedMap: Record = { + 0: ( + + ), + 1: ( + + ), + }; + + 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, + startTimeInMillis, + endTimeInMillis, + durationInMillis, + uuid, + } = snapshotDetails; + + const indicesList = indices.length ? ( +
    + {indices.map((index: string) => ( +
  • + + {index} + +
  • + ))} +
+ ) : ( + + ); + + return ( + + + + + + + + + {version} / {versionId} + + + + + + + + + + {uuid} + + + + + + + + + + + + + + + + + + + + + + {includeGlobalStateToHumanizedMap[includeGlobalState]} + + + + + + + + + + + + {indicesList} + + + + + + + + + + + + + {formatDate(startTimeInMillis)} + + + + + + + + + + + {formatDate(endTimeInMillis)} + + + + + + + + + + + + + + + + + + + ); +}; 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 8fbe547f621af..bc13f67d7bea7 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 @@ -7,12 +7,14 @@ import React, { Fragment } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiLink, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; + 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 { linkToRepositories } from '../../../services/navigation'; import { SnapshotDetails } from './snapshot_details'; import { SnapshotTable } from './snapshot_table'; @@ -37,7 +39,7 @@ export const SnapshotList: React.FunctionComponent ); - } else if (snapshots && snapshots.length === 0) { + } else if (snapshots.length === 0) { content = ( ); } else { + const repositoryErrorsWarning = Object.keys(errors).length ? ( + + } + color="warning" + iconType="alert" + > + + + + ), + }} + /> + + ) : null; + content = ( - + + {repositoryErrorsWarning} + + + + + ); } 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 index e7fded3807260..fee267b210b02 100644 --- 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 @@ -12,15 +12,19 @@ import { EuiButton, EuiInMemoryTable, EuiLink } from '@elastic/eui'; import { SnapshotDetails } from '../../../../../../common/types'; import { useAppDependencies } from '../../../../index'; import { formatDate } from '../../../../services/text'; +import { linkToRepository } from '../../../../services/navigation'; +import { DataPlaceholder } from '../../../../components'; interface Props extends RouteComponentProps { snapshots: SnapshotDetails[]; + repositories: string[]; reload: () => Promise; openSnapshotDetails: (repositoryName: string, snapshotId: string) => void; } const SnapshotTableUi: React.FunctionComponent = ({ snapshots, + repositories, reload, openSnapshotDetails, history, @@ -52,7 +56,9 @@ const SnapshotTableUi: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - render: (repository: string) => repository, + render: (repositoryName: string) => ( + {repositoryName} + ), }, { field: 'startTimeInMillis', @@ -61,7 +67,9 @@ const SnapshotTableUi: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - render: (startTimeInMillis: number) => formatDate(startTimeInMillis), + render: (startTimeInMillis: number) => ( + {formatDate(startTimeInMillis)} + ), }, { field: 'durationInMillis', @@ -72,11 +80,13 @@ const SnapshotTableUi: React.FunctionComponent = ({ sortable: true, width: '100px', render: (durationInMillis: number) => ( - + + + ), }, { @@ -137,6 +147,18 @@ const SnapshotTableUi: React.FunctionComponent = ({ incremental: true, schema: true, }, + filters: [ + { + type: 'field_value_selection', + field: 'repository', + name: 'Repository', + multiSelect: false, + options: repositories.map(repository => ({ + value: repository, + view: repository, + })), + }, + ], }; return ( diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx index 1eb051b81084d..0ac80c122caa8 100644 --- a/x-pack/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx @@ -13,7 +13,7 @@ import { Repository } from '../../../../common/types'; import { RepositoryForm, SectionError } from '../../components'; import { BASE_PATH, Section } from '../../constants'; import { useAppDependencies } from '../../index'; -import { breadcrumbService } from '../../services/breadcrumb'; +import { breadcrumbService } from '../../services/navigation'; import { addRepository } from '../../services/http'; export const RepositoryAdd: React.FunctionComponent = ({ history }) => { diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/repository_edit/repository_edit.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/repository_edit/repository_edit.tsx index ecc9ff381f05a..8e5e78952aa7c 100644 --- a/x-pack/plugins/snapshot_restore/public/app/sections/repository_edit/repository_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/app/sections/repository_edit/repository_edit.tsx @@ -13,7 +13,7 @@ import { Repository } from '../../../../common/types'; import { RepositoryForm, SectionError, SectionLoading } from '../../components'; import { BASE_PATH, Section } from '../../constants'; import { useAppDependencies } from '../../index'; -import { breadcrumbService } from '../../services/breadcrumb'; +import { breadcrumbService } from '../../services/navigation'; import { editRepository, loadRepository } from '../../services/http'; interface MatchParams { diff --git a/x-pack/plugins/snapshot_restore/public/app/services/breadcrumb/breadcrumb.ts b/x-pack/plugins/snapshot_restore/public/app/services/navigation/breadcrumb.ts similarity index 99% rename from x-pack/plugins/snapshot_restore/public/app/services/breadcrumb/breadcrumb.ts rename to x-pack/plugins/snapshot_restore/public/app/services/navigation/breadcrumb.ts index 59d60d267186b..18daf4ee8162d 100644 --- a/x-pack/plugins/snapshot_restore/public/app/services/breadcrumb/breadcrumb.ts +++ b/x-pack/plugins/snapshot_restore/public/app/services/navigation/breadcrumb.ts @@ -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 { BASE_PATH } from '../../constants'; import { textService } from '../text'; diff --git a/x-pack/plugins/snapshot_restore/public/app/services/breadcrumb/index.ts b/x-pack/plugins/snapshot_restore/public/app/services/navigation/index.ts similarity index 82% rename from x-pack/plugins/snapshot_restore/public/app/services/breadcrumb/index.ts rename to x-pack/plugins/snapshot_restore/public/app/services/navigation/index.ts index c096016646e71..3350af1ee1101 100644 --- a/x-pack/plugins/snapshot_restore/public/app/services/breadcrumb/index.ts +++ b/x-pack/plugins/snapshot_restore/public/app/services/navigation/index.ts @@ -5,3 +5,4 @@ */ export { breadcrumbService } from './breadcrumb'; +export { linkToRepository, linkToRepositories } from './links'; diff --git a/x-pack/plugins/snapshot_restore/public/app/services/navigation/links.ts b/x-pack/plugins/snapshot_restore/public/app/services/navigation/links.ts new file mode 100644 index 0000000000000..b87259c0c1c7f --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/services/navigation/links.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BASE_PATH } from '../../constants'; + +export function linkToRepositories() { + return `#${BASE_PATH}/repositories`; +} + +export function linkToRepository(repositoryName: string) { + return `#${BASE_PATH}/repositories/${repositoryName}`; +} diff --git a/x-pack/plugins/snapshot_restore/public/plugin.ts b/x-pack/plugins/snapshot_restore/public/plugin.ts index 4989c9b27d545..248860afae944 100644 --- a/x-pack/plugins/snapshot_restore/public/plugin.ts +++ b/x-pack/plugins/snapshot_restore/public/plugin.ts @@ -11,7 +11,7 @@ import { AppCore, AppPlugins } from './app/types'; import template from './index.html'; import { Core, Plugins } from './shim'; -import { breadcrumbService } from './app/services/breadcrumb'; +import { breadcrumbService } from './app/services/navigation'; import { documentationLinksService } from './app/services/documentation'; import { httpService } from './app/services/http'; import { textService } from './app/services/text'; diff --git a/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.test.ts b/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.test.ts new file mode 100644 index 0000000000000..ae3735f28529a --- /dev/null +++ b/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { deserializeSnapshotDetails } from './snapshot_serialization'; + +describe('deserializeSnapshotDetails', () => { + test('deserializes a snapshot', () => { + expect( + deserializeSnapshotDetails('repositoryName', { + snapshot: 'snapshot name', + uuid: 'UUID', + version_id: 5, + version: 'version', + indices: ['index2', 'index3', 'index1'], + include_global_state: false, + state: 'SUCCESS', + start_time: '0', + start_time_in_millis: 0, + end_time: '1', + end_time_in_millis: 1, + duration_in_millis: 1, + shards: { + total: 3, + failed: 1, + successful: 2, + }, + failures: [ + { + index: 'z', + shard: 1, + }, + { + index: 'a', + shard: 3, + }, + { + index: 'a', + shard: 1, + }, + { + index: 'a', + shard: 2, + }, + ], + }) + ).toEqual({ + repository: 'repositoryName', + snapshot: 'snapshot name', + uuid: 'UUID', + versionId: 5, + version: 'version', + // Indices are sorted. + indices: ['index1', 'index2', 'index3'], + // Converted from a boolean into 0 or 1. + includeGlobalState: 0, + // Failures are grouped and sorted by index, and the failures themselves are sorted by shard. + indexFailures: [ + { + index: 'a', + failures: [ + { + shard: 1, + }, + { + shard: 2, + }, + { + shard: 3, + }, + ], + }, + { + index: 'z', + failures: [ + { + shard: 1, + }, + ], + }, + ], + state: 'SUCCESS', + startTime: '0', + startTimeInMillis: 0, + endTime: '1', + endTimeInMillis: 1, + durationInMillis: 1, + shards: { + total: 3, + failed: 1, + successful: 2, + }, + }); + }); +}); 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 c623d062400db..6965654cffb0c 100644 --- a/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.ts +++ b/x-pack/plugins/snapshot_restore/server/lib/snapshot_serialization.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { sortBy } from 'lodash'; + import { SnapshotDetails } from '../../common/types'; import { SnapshotDetailsEs } from '../types'; @@ -20,7 +22,7 @@ export function deserializeSnapshotDetails( uuid, version_id: versionId, version, - indices, + indices = [], include_global_state: includeGlobalState, state, start_time: startTime, @@ -28,17 +30,42 @@ export function deserializeSnapshotDetails( end_time: endTime, end_time_in_millis: endTimeInMillis, duration_in_millis: durationInMillis, - failures, + failures = [], shards, } = snapshotDetailsEs; + // If an index has multiple failures, we'll want to see them grouped together. + const indexToFailuresMap = failures.reduce((map, failure) => { + const { index, ...rest } = failure; + if (!map[index]) { + map[index] = { + index, + failures: [], + }; + } + + map[index].failures.push(rest); + return map; + }, {}); + + // Sort all failures by their shard. + Object.keys(indexToFailuresMap).forEach(index => { + indexToFailuresMap[index].failures = sortBy( + indexToFailuresMap[index].failures, + ({ shard }) => shard + ); + }); + + // Sort by index name. + const indexFailures = sortBy(Object.values(indexToFailuresMap), ({ index }) => index); + return { repository, snapshot, uuid, versionId, version, - indices, + indices: [...indices].sort(), includeGlobalState: Boolean(includeGlobalState) ? 1 : 0, state, startTime, @@ -46,7 +73,7 @@ export function deserializeSnapshotDetails( endTime, endTimeInMillis, durationInMillis, - failures, + indexFailures, shards, }; } 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 c4a2bc6f86134..b5ef21963c0c5 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 @@ -13,7 +13,7 @@ const defaultSnapshot = { uuid: undefined, versionId: undefined, version: undefined, - indices: undefined, + indices: [], includeGlobalState: 0, state: undefined, startTime: undefined, @@ -21,7 +21,7 @@ const defaultSnapshot = { endTime: undefined, endTimeInMillis: undefined, durationInMillis: undefined, - failures: undefined, + indexFailures: [], shards: undefined, }; @@ -60,7 +60,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { .mockReturnValueOnce(mockGetSnapshotsBarResponse); const expectedResponse = { - errors: [], + errors: {}, + repositories: ['fooRepository', 'barRepository'], snapshots: [ { ...defaultSnapshot, repository: 'fooRepository', snapshot: 'snapshot1' }, { ...defaultSnapshot, repository: 'barRepository', snapshot: 'snapshot2' }, @@ -74,7 +75,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { test('returns empty arrays if no snapshots returned from ES', async () => { const mockSnapshotGetRepositoryEsResponse = {}; const callWithRequest = jest.fn().mockReturnValue(mockSnapshotGetRepositoryEsResponse); - const expectedResponse = { errors: [], snapshots: [] }; + const expectedResponse = { errors: [], snapshots: [], repositories: [] }; const response = await getAllHandler(mockRequest, 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 6cac8134d32dd..02102b4cdaffb 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -19,6 +19,7 @@ export const getAllHandler: RouterRouteHandler = async ( ): Promise<{ snapshots: SnapshotDetails[]; errors: any[]; + repositories: string[]; }> => { const repositoriesByName = await callWithRequest('snapshot.getRepository', { repository: '_all', @@ -27,11 +28,12 @@ export const getAllHandler: RouterRouteHandler = async ( const repositoryNames = Object.keys(repositoriesByName); if (repositoryNames.length === 0) { - return { snapshots: [], errors: [] }; + return { snapshots: [], errors: [], repositories: [] }; } const snapshots: SnapshotDetails[] = []; - const errors: any = []; + const errors: any = {}; + const repositories: string[] = []; const fetchSnapshotsForRepository = async (repository: string) => { try { @@ -48,10 +50,12 @@ export const getAllHandler: RouterRouteHandler = async ( fetchedSnapshots.forEach((snapshot: SnapshotDetailsEs) => { snapshots.push(deserializeSnapshotDetails(repository, snapshot)); }); + + repositories.push(repository); } 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); + errors[repository] = error; } }; @@ -59,6 +63,7 @@ export const getAllHandler: RouterRouteHandler = async ( return { snapshots, + repositories, errors, }; }; diff --git a/x-pack/plugins/snapshot_restore/server/types/snapshot.ts b/x-pack/plugins/snapshot_restore/server/types/snapshot.ts index 280fcc56ba559..d88c6258ae108 100644 --- a/x-pack/plugins/snapshot_restore/server/types/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/server/types/snapshot.ts @@ -19,7 +19,7 @@ export interface SnapshotDetailsEs { end_time: string; end_time_in_millis: number; duration_in_millis: number; - failures: string[]; + failures: any[]; shards: SnapshotDetailsShardsStatusEs; }