diff --git a/ee/app/assets/javascripts/external_issues_list/components/external_issues_list_root.vue b/ee/app/assets/javascripts/external_issues_list/components/external_issues_list_root.vue index 7f631caa213e1047b8e7fd9722aed04ca9a8debf..2016cfa157cfcd86665af5a3cf5ea7f7c942762e 100644 --- a/ee/app/assets/javascripts/external_issues_list/components/external_issues_list_root.vue +++ b/ee/app/assets/javascripts/external_issues_list/components/external_issues_list_root.vue @@ -1,7 +1,14 @@ <script> -import { GlButton, GlIcon, GlLink, GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { + GlButton, + GlIcon, + GlLink, + GlSprintf, + GlSafeHtmlDirective as SafeHtml, + GlAlert, +} from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; -import createFlash from '~/flash'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import { IssuableStates, @@ -30,6 +37,7 @@ export default { GlIcon, GlLink, GlSprintf, + GlAlert, IssuableList, ExternalIssuesListEmptyState, }, @@ -70,6 +78,7 @@ export default { [IssuableStates.Closed]: 0, [IssuableStates.All]: 0, }, + errorMessage: null, }; }, computed: { @@ -173,11 +182,9 @@ export default { return filteredSearchValue; }, onExternalIssuesQueryError(error, message) { - createFlash({ - message: message || error.message, - captureError: true, - error, - }); + this.errorMessage = message || error.message; + + Sentry.captureException(error); }, onIssuableListClickTab(selectedIssueState) { this.currentPage = 1; @@ -225,7 +232,11 @@ export default { </script> <template> + <gl-alert v-if="errorMessage" class="gl-mt-3" variant="danger" :dismissible="false"> + {{ errorMessage }} + </gl-alert> <issuable-list + v-else :namespace="projectFullPath" :tabs="$options.IssuableListTabs" :current-tab="currentState" diff --git a/ee/spec/frontend/external_issues_list/components/external_issues_list_root_spec.js b/ee/spec/frontend/external_issues_list/components/external_issues_list_root_spec.js index 6e62fb633b686a656e2ab0a37758f497a2313a6b..ac87cc920818629d4923f62c9440e76431662eb9 100644 --- a/ee/spec/frontend/external_issues_list/components/external_issues_list_root_spec.js +++ b/ee/spec/frontend/external_issues_list/components/external_issues_list_root_spec.js @@ -1,3 +1,5 @@ +import { GlAlert } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { shallowMount, createLocalVue, mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import VueApollo from 'vue-apollo'; @@ -8,7 +10,6 @@ import jiraIssuesResolver from 'ee/integrations/jira/issues_list/graphql/resolve import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import { i18n } from '~/issues_list/constants'; import axios from '~/lib/utils/axios_utils'; @@ -61,9 +62,21 @@ describe('ExternalIssuesListRoot', () => { const mockLabel = 'ecosystem'; const findIssuableList = () => wrapper.findComponent(IssuableList); + const findAlert = () => wrapper.findComponent(GlAlert); const createLabelFilterEvent = (data) => ({ type: 'labels', value: { data } }); const createSearchFilterEvent = (data) => ({ type: 'filtered-search-term', value: { data } }); + const expectErrorHandling = (expectedRenderedErrorMessage) => { + const issuesList = findIssuableList(); + const alert = findAlert(); + + expect(issuesList.exists()).toBe(false); + + expect(alert.exists()).toBe(true); + expect(alert.text()).toBe(expectedRenderedErrorMessage); + expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error)); + }; + const createComponent = ({ apolloProvider = createMockApolloProvider(), provide = mockProvide, @@ -300,13 +313,17 @@ describe('ExternalIssuesListRoot', () => { }); describe('error handling', () => { + beforeEach(() => { + jest.spyOn(Sentry, 'captureException'); + }); + describe('when request fails', () => { it.each` APIErrors | expectedRenderedErrorMessage ${['API error']} | ${'API error'} ${undefined} | ${i18n.errorFetchingIssues} `( - 'calls `createFlash` with "$expectedRenderedErrorMessage" when API responds with "$APIErrors"', + 'displays error alert with "$expectedRenderedErrorMessage" when API responds with "$APIErrors"', async ({ APIErrors, expectedRenderedErrorMessage }) => { jest.spyOn(axios, 'get'); mock @@ -316,17 +333,13 @@ describe('ExternalIssuesListRoot', () => { createComponent(); await waitForPromises(); - expect(createFlash).toHaveBeenCalledWith({ - message: expectedRenderedErrorMessage, - captureError: true, - error: expect.any(Object), - }); + expectErrorHandling(expectedRenderedErrorMessage); }, ); }); describe('when GraphQL network error is encountered', () => { - it('calls `createFlash` correctly with default error message', async () => { + it('displays error alert with default error message', async () => { createComponent({ apolloProvider: createMockApolloProvider({ Query: { @@ -336,35 +349,24 @@ describe('ExternalIssuesListRoot', () => { }); await waitForPromises(); - expect(createFlash).toHaveBeenCalledWith({ - message: i18n.errorFetchingIssues, - captureError: true, - error: expect.any(Object), - }); + expectErrorHandling(i18n.errorFetchingIssues); }); }); }); describe('pagination', () => { it.each` - scenario | issuesListLoadFailed | issues | shouldShowPaginationControls - ${'fails'} | ${true} | ${[]} | ${false} - ${'returns no issues'} | ${false} | ${[]} | ${false} - ${`returns some issues`} | ${false} | ${mockExternalIssues} | ${true} + scenario | issues | shouldShowPaginationControls + ${'returns no issues'} | ${[]} | ${false} + ${`returns some issues`} | ${mockExternalIssues} | ${true} `( 'sets `showPaginationControls` prop to $shouldShowPaginationControls when request $scenario', - async ({ issuesListLoadFailed, issues, shouldShowPaginationControls }) => { + async ({ issues, shouldShowPaginationControls }) => { jest.spyOn(axios, 'get'); - mock - .onGet(mockProvide.issuesFetchPath) - .replyOnce( - issuesListLoadFailed ? httpStatus.INTERNAL_SERVER_ERROR : httpStatus.OK, - issues, - { - 'x-page': 1, - 'x-total': issues.length, - }, - ); + mock.onGet(mockProvide.issuesFetchPath).replyOnce(httpStatus.OK, issues, { + 'x-page': 1, + 'x-total': issues.length, + }); createComponent(); await waitForPromises();