diff --git a/src/platform/plugins/shared/es_ui_shared/__packages_do_not_import__/authorization/types.ts b/src/platform/plugins/shared/es_ui_shared/__packages_do_not_import__/authorization/types.ts index 9b05303cb82f4..c2574598b6d73 100644 --- a/src/platform/plugins/shared/es_ui_shared/__packages_do_not_import__/authorization/types.ts +++ b/src/platform/plugins/shared/es_ui_shared/__packages_do_not_import__/authorization/types.ts @@ -20,4 +20,10 @@ export interface Error { error: string; cause?: string[]; message?: string; + statusCode?: number; + attributes?: { + error?: { + type?: string; + }; + }; } diff --git a/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts b/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts index 835566b19c4e3..e3200b12fd223 100644 --- a/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts @@ -14,6 +14,11 @@ type HttpResponse = Record | any[]; export interface ResponseError { statusCode: number; message: string | Error; + attributes?: { + error?: { + type?: string; + }; + }; } // Register helpers to mock HTTP Requests diff --git a/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/home.test.ts index a25dcf2a61341..cb558d701cda0 100644 --- a/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/home.test.ts @@ -7,7 +7,10 @@ import { act } from 'react-dom/test-utils'; import * as fixtures from '../../test/fixtures'; -import { SNAPSHOT_STATE } from '../../public/application/constants'; +import { + SNAPSHOT_REPOSITORY_EXCEPTION_ERROR, + SNAPSHOT_STATE, +} from '../../public/application/constants'; import { API_BASE_PATH } from '../../common'; import { setupEnvironment, pageHelpers, getRandomString, findTestSubject } from './helpers'; import { HomeTestBed } from './helpers/home.helpers'; @@ -418,7 +421,8 @@ describe('', () => { describe('snapshots', () => { describe('when there are no snapshots nor repositories', () => { beforeAll(() => { - httpRequestsMockHelpers.setLoadSnapshotsResponse({ snapshots: [], repositories: [] }); + httpRequestsMockHelpers.setLoadSnapshotsResponse({ snapshots: [] }); + httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories: [] }); }); beforeEach(async () => { @@ -448,9 +452,11 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadSnapshotsResponse({ snapshots: [], - repositories: ['my-repo'], total: 0, }); + httpRequestsMockHelpers.setLoadRepositoriesResponse({ + repositories: [{ name: 'my-repo' }], + }); testBed = await setup(httpSetup); @@ -489,9 +495,11 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadSnapshotsResponse({ snapshots, - repositories: [REPOSITORY_NAME], total: 2, }); + httpRequestsMockHelpers.setLoadRepositoriesResponse({ + repositories: [{ name: REPOSITORY_NAME }], + }); testBed = await setup(httpSetup); @@ -528,7 +536,6 @@ describe('', () => { httpRequestsMockHelpers.setLoadSnapshotsResponse({ snapshots, total: 2, - repositories: [REPOSITORY_NAME], errors: { repository_with_errors: { type: 'repository_exception', @@ -537,6 +544,9 @@ describe('', () => { }, }, }); + httpRequestsMockHelpers.setLoadRepositoriesResponse({ + repositories: [{ name: REPOSITORY_NAME }], + }); testBed = await setup(httpSetup); @@ -553,32 +563,6 @@ describe('', () => { ); }); - test('should show a prompt if a repository contains errors and there are no other repositories', async () => { - httpRequestsMockHelpers.setLoadSnapshotsResponse({ - snapshots, - repositories: [], - errors: { - repository_with_errors: { - type: 'repository_exception', - reason: - '[repository_with_errors] Could not read repository data because the contents of the repository do not match its expected state.', - }, - }, - }); - - testBed = await setup(httpSetup); - - await act(async () => { - testBed.actions.selectTab('snapshots'); - }); - - testBed.component.update(); - - const { find, exists } = testBed; - expect(exists('repositoryErrorsPrompt')).toBe(true); - expect(find('repositoryErrorsPrompt').text()).toContain('Some repositories contain errors'); - }); - test('each row should have a link to the repository', async () => { const { component, find, exists, table, router } = testBed; @@ -886,5 +870,68 @@ describe('', () => { }); }); }); + + describe('when there is an error while fetching the snapshots', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadSnapshotsResponse(undefined, { + statusCode: 500, + message: '[repository_with_errors] cannot retrieve snapshots list from this repository', + }); + httpRequestsMockHelpers.setLoadRepositoriesResponse({ + repositories: [{ name: REPOSITORY_NAME }, { name: 'repository_with_errors' }], + }); + + testBed = await setup(httpSetup); + + await act(async () => { + testBed.actions.selectTab('snapshots'); + }); + + testBed.component.update(); + }); + + test('should show a generic error prompt if snapshots request fails while still showing the search bar', async () => { + const { find, exists } = testBed; + + // Check that the search bar is still present + expect(exists('snapshotListSearch')).toBe(true); + + // Check that the error message is displayed + expect(exists('snapshotsLoadingError')).toBe(true); + expect(find('snapshotsLoadingError').text()).toContain('Error loading snapshots'); + }); + + test('should show a repository error prompt if snapshots request fails due to repository exception while still showing the search bar', async () => { + httpRequestsMockHelpers.setLoadSnapshotsResponse(undefined, { + statusCode: 500, + message: '[repository_with_errors] cannot retrieve snapshots list from this repository', + attributes: { + error: { + type: SNAPSHOT_REPOSITORY_EXCEPTION_ERROR, + }, + }, + }); + httpRequestsMockHelpers.setLoadRepositoriesResponse({ + repositories: [{ name: REPOSITORY_NAME }, { name: 'repository_with_errors' }], + }); + + testBed = await setup(httpSetup); + + await act(async () => { + testBed.actions.selectTab('snapshots'); + }); + + testBed.component.update(); + + const { find, exists } = testBed; + + // Check that the search bar is still present + expect(exists('snapshotListSearch')).toBe(true); + + // Check that the error message is displayed + expect(exists('repositoryErrorsPrompt')).toBe(true); + expect(find('repositoryErrorsPrompt').text()).toContain('Some repositories contain errors'); + }); + }); }); }); diff --git a/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/snapshot_list.test.tsx b/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/snapshot_list.test.tsx index 8d4133ee48373..284f8a19008f2 100644 --- a/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/snapshot_list.test.tsx +++ b/x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/snapshot_list.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { EuiSearchBoxProps } from '@elastic/eui/src/components/search_bar/search_box'; -import { useLoadSnapshots } from '../../public/application/services/http'; +import { useLoadRepositories, useLoadSnapshots } from '../../public/application/services/http'; import { DEFAULT_SNAPSHOT_LIST_PARAMS } from '../../public/application/lib'; import * as fixtures from '../../test/fixtures'; @@ -26,6 +26,7 @@ import { pageHelpers, getRandomString } from './helpers'; */ jest.mock('../../public/application/services/http', () => ({ useLoadSnapshots: jest.fn(), + useLoadRepositories: jest.fn(), setUiMetricServiceSnapshot: () => {}, setUiMetricService: () => {}, })); @@ -67,13 +68,24 @@ describe('', () => { isLoading: false, data: { snapshots, - repositories: [REPOSITORY_NAME], policies: [], errors: {}, total: snapshots.length, }, resendRequest: () => {}, }); + (useLoadRepositories as jest.Mock).mockReturnValue({ + error: null, + isInitialRequest: false, + isLoading: false, + data: { + repositories: [ + { + name: REPOSITORY_NAME, + }, + ], + }, + }); }); afterAll(() => { diff --git a/x-pack/platform/plugins/private/snapshot_restore/public/application/constants/index.ts b/x-pack/platform/plugins/private/snapshot_restore/public/application/constants/index.ts index 7ca0e3181aea0..aee540d56d087 100644 --- a/x-pack/platform/plugins/private/snapshot_restore/public/application/constants/index.ts +++ b/x-pack/platform/plugins/private/snapshot_restore/public/application/constants/index.ts @@ -19,6 +19,8 @@ export enum SNAPSHOT_STATE { PARTIAL = 'PARTIAL', } +export const SNAPSHOT_REPOSITORY_EXCEPTION_ERROR = 'repository_exception'; + export enum SLM_STATE { RUNNING = 'RUNNING', STOPPING = 'STOPPING', diff --git a/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_error.tsx b/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_error.tsx index 08d2b9589e803..4f894437cf33c 100644 --- a/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_error.tsx +++ b/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/components/repository_error.tsx @@ -8,11 +8,15 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiLink, EuiPageTemplate } from '@elastic/eui'; +import { EuiLink, EuiPageTemplate, EuiSpacer } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../shared_imports'; import { linkToRepositories } from '../../../../services/navigation'; -export const RepositoryError: React.FunctionComponent = () => { +interface RepositoryErrorProps { + errorMessage?: string; +} + +export const RepositoryError = ({ errorMessage }: RepositoryErrorProps) => { const history = useHistory(); return ( { } body={

+ {errorMessage} + diff --git a/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx b/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx index 082c0775b6dfc..da9651b0d22a1 100644 --- a/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx +++ b/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; @@ -55,6 +55,7 @@ interface Props { setListParams: (listParams: SnapshotListParams) => void; totalItemCount: number; isLoading: boolean; + error?: ReactNode; } export const SnapshotTable: React.FunctionComponent = (props: Props) => { @@ -67,6 +68,7 @@ export const SnapshotTable: React.FunctionComponent = (props: Props) => { setListParams, totalItemCount, isLoading, + error, } = props; const { i18n, uiMetricService, history } = useServices(); const [selectedItems, setSelectedItems] = useState([]); @@ -324,33 +326,37 @@ export const SnapshotTable: React.FunctionComponent = (props: Props) => { onSnapshotDeleted={onSnapshotDeleted} repositories={repositories} /> - ) => { - const { page: { index, size } = {}, sort: { field, direction } = {} } = criteria; + {error ? ( + error + ) : ( + ) => { + const { page: { index, size } = {}, sort: { field, direction } = {} } = criteria; - setListParams({ - ...listParams, - sortField: (field as SortField) ?? listParams.sortField, - sortDirection: (direction as SortDirection) ?? listParams.sortDirection, - pageIndex: index ?? listParams.pageIndex, - pageSize: size ?? listParams.pageSize, - }); - }} - loading={isLoading} - selection={selection} - pagination={pagination} - rowProps={() => ({ - 'data-test-subj': 'row', - })} - cellProps={() => ({ - 'data-test-subj': 'cell', - })} - data-test-subj="snapshotTable" - /> + setListParams({ + ...listParams, + sortField: (field as SortField) ?? listParams.sortField, + sortDirection: (direction as SortDirection) ?? listParams.sortDirection, + pageIndex: index ?? listParams.pageIndex, + pageSize: size ?? listParams.pageSize, + }); + }} + loading={isLoading} + selection={selection} + pagination={pagination} + rowProps={() => ({ + 'data-test-subj': 'row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + data-test-subj="snapshotTable" + /> + )} ); }; diff --git a/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx index 0ff63cd990d6e..cacdc4d8dc9e4 100644 --- a/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/platform/plugins/private/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx @@ -18,8 +18,12 @@ import { reactRouterNavigate, useExecutionContext, } from '../../../../shared_imports'; -import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants'; -import { useLoadSnapshots } from '../../../services/http'; +import { + BASE_PATH, + SNAPSHOT_REPOSITORY_EXCEPTION_ERROR, + UIM_SNAPSHOT_LIST_LOAD, +} from '../../../constants'; +import { useLoadRepositories, useLoadSnapshots } from '../../../services/http'; import { linkToRepositories } from '../../../services/navigation'; import { useAppContext, useServices } from '../../../app_context'; import { useDecodedParams, SnapshotListParams, DEFAULT_SNAPSHOT_LIST_PARAMS } from '../../../lib'; @@ -45,18 +49,23 @@ export const SnapshotList: React.FunctionComponent(DEFAULT_SNAPSHOT_LIST_PARAMS); const { error, - isInitialRequest, - isLoading, - data: { - snapshots = [], - repositories = [], - policies = [], - errors = {}, - total: totalSnapshotsCount, - }, + isInitialRequest: isSnapshotsInitialRequest, + isLoading: isSnapshotsLoading, + data: { snapshots = [], policies = [], errors = {}, total: totalSnapshotsCount }, resendRequest: reload, } = useLoadSnapshots(listParams); + // To make the repository filter work in the search bar (even when snapshots request fails), we need to load repositories separately. + // For more context see https://github.com/elastic/kibana/issues/225935 + const { + isInitialRequest: isRepositoriesInitialRequest, + isLoading: isRepositoriesLoading, + data: { repositories = [] }, + } = useLoadRepositories(); + + const isInitialRequest = isSnapshotsInitialRequest && isRepositoriesInitialRequest; + const isLoading = isSnapshotsLoading || isRepositoriesLoading; + const repositoriesNames = repositories.map((repository: { name: string }) => repository?.name); const { uiMetricService } = useServices(); const { core } = useAppContext(); @@ -133,25 +142,32 @@ export const SnapshotList: React.FunctionComponent ); - } else if (error) { - content = ( - - } - error={error as Error} - /> - ); - } else if (Object.keys(errors).length && repositories.length === 0) { - content = ; - } else if (repositories.length === 0) { + } else if (!error && repositoriesNames.length === 0) { content = ; - } else if (totalSnapshotsCount === 0 && !listParams.searchField && !isLoading) { + } else if (!error && totalSnapshotsCount === 0 && !listParams.searchField && !isLoading) { content = ; } else { + let snapshotsLoadingError = null; + + if (error) { + if (error?.attributes?.error?.type === SNAPSHOT_REPOSITORY_EXCEPTION_ERROR) { + snapshotsLoadingError = ; + } else { + snapshotsLoadingError = ( + + } + data-test-subj="snapshotsLoadingError" + error={error as Error} + /> + ); + } + } + const repositoryErrorsWarning = Object.keys(errors).length ? ( <> );