From 47a8580737b95f9d934fb684bab27a13e16870fb Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Tue, 10 Aug 2021 12:09:03 +0200 Subject: [PATCH] SONAR-15177 Improve code viewer when no component is analysis --- .../sonar-web/src/main/js/apps/code/code.css | 5 + .../code/components/{App.tsx => AppCode.tsx} | 30 +- .../code/components/__tests__/App-test.tsx | 108 --- .../components/__tests__/AppCode-test.tsx | 198 +++++ .../__tests__/__snapshots__/App-test.tsx.snap | 25 - .../__snapshots__/AppCode-test.tsx.snap | 765 ++++++++++++++++++ .../sonar-web/src/main/js/apps/code/routes.ts | 2 +- .../resources/org/sonar/l10n/core.properties | 4 + 8 files changed, 995 insertions(+), 142 deletions(-) rename server/sonar-web/src/main/js/apps/code/components/{App.tsx => AppCode.tsx} (93%) delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/App-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/code/code.css b/server/sonar-web/src/main/js/apps/code/code.css index 734b3128f51..39d4ff9255b 100644 --- a/server/sonar-web/src/main/js/apps/code/code.css +++ b/server/sonar-web/src/main/js/apps/code/code.css @@ -99,3 +99,8 @@ table > thead > tr.code-components-header > th { height: 8px; width: 4px; } + +.code-components .no-file .h1 { + position: fixed; + top: 50%; +} diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/AppCode.tsx similarity index 93% rename from server/sonar-web/src/main/js/apps/code/components/App.tsx rename to server/sonar-web/src/main/js/apps/code/components/AppCode.tsx index 6215ebc309d..33d3f562810 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/AppCode.tsx @@ -71,7 +71,7 @@ interface State { total: number; } -export class App extends React.PureComponent { +export class AppCode extends React.PureComponent { mounted = false; state: State; @@ -255,6 +255,8 @@ export class App extends React.PureComponent { const showSearch = searchResults !== undefined; + const hasNoFile = components.length === 0 && searchResults === undefined; + const shouldShowBreadcrumbs = breadcrumbs.length > 1 && !showSearch; const shouldShowComponentList = sourceViewer === undefined && components.length > 0 && !showSearch; @@ -278,14 +280,26 @@ export class App extends React.PureComponent { /> - + {!hasNoFile && ( + + )}
+ {hasNoFile && ( +
+ + {translate( + 'code_viewer.no_source_code_displayed_due_to_empty_analysis', + component.qualifier + )} + +
+ )} {shouldShowBreadcrumbs && ( ( mapStateToProps, mapDispatchToProps -)(App); +)(AppCode); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx deleted file mode 100644 index bdf98a7a8d8..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/App-test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2021 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; -import { mockIssue, mockRouter } from '../../../../helpers/testMocks'; -import { retrieveComponent } from '../../utils'; -import { App } from '../App'; - -jest.mock('../../utils', () => ({ - retrieveComponent: jest.fn().mockResolvedValue({ - breadcrumbs: [], - component: { qualifier: 'APP' }, - components: [], - page: 0, - total: 1 - }), - retrieveComponentChildren: () => Promise.resolve() -})); - -const METRICS = { - coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, - new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } -}; - -beforeEach(() => { - (retrieveComponent as jest.Mock).mockClear(); -}); - -it('should have correct title for APP based component', async () => { - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(wrapper.find('Helmet')).toMatchSnapshot(); -}); - -it('should have correct title for portfolio base component', async () => { - (retrieveComponent as jest.Mock).mockResolvedValueOnce({ - breadcrumbs: [], - component: { qualifier: 'VW' }, - components: [], - page: 0, - total: 1 - }); - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(wrapper.find('Helmet')).toMatchSnapshot(); -}); - -it('should have correct title for project component', async () => { - (retrieveComponent as jest.Mock).mockResolvedValueOnce({ - breadcrumbs: [], - component: { qualifier: 'TRK' }, - components: [], - page: 0, - total: 1 - }); - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(wrapper.find('Helmet')).toMatchSnapshot(); -}); - -it('should refresh branch status if issues are updated', async () => { - const fetchBranchStatus = jest.fn(); - const branchLike = mockPullRequest(); - const wrapper = shallowRender({ branchLike, fetchBranchStatus }); - const instance = wrapper.instance(); - await waitAndUpdate(wrapper); - - instance.handleIssueChange(mockIssue()); - expect(fetchBranchStatus).toBeCalledWith(branchLike, 'foo'); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx new file mode 100644 index 00000000000..8db2522d690 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/AppCode-test.tsx @@ -0,0 +1,198 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; +import { + mockComponent, + mockComponentMeasure, + mockIssue, + mockRouter +} from '../../../../helpers/testMocks'; +import { ComponentQualifier } from '../../../../types/component'; +import { loadMoreChildren, retrieveComponent } from '../../utils'; +import { AppCode } from '../AppCode'; + +jest.mock('../../utils', () => ({ + loadMoreChildren: jest.fn().mockResolvedValue({}), + retrieveComponent: jest.fn().mockResolvedValue({ + breadcrumbs: [], + component: { qualifier: 'APP' }, + components: [], + page: 0, + total: 1 + }), + retrieveComponentChildren: () => Promise.resolve() +})); + +const METRICS = { + coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, + new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } +}; + +beforeEach(() => { + (retrieveComponent as jest.Mock).mockClear(); +}); + +it.each([ + [ComponentQualifier.Application], + [ComponentQualifier.Project], + [ComponentQualifier.Portfolio], + [ComponentQualifier.SubPortfolio] +])('should render correclty when no sub component for %s', async qualifier => { + const component = { breadcrumbs: [], name: 'foo', key: 'foo', qualifier }; + (retrieveComponent as jest.Mock).mockResolvedValueOnce({ + breadcrumbs: [], + component, + components: [], + page: 0, + total: 1 + }); + const wrapper = shallowRender({ component }); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + wrapper.instance().handleSearchResults([]); + expect(wrapper).toMatchSnapshot('no search'); + (retrieveComponent as jest.Mock).mockResolvedValueOnce({ + breadcrumbs: [], + component, + components: [mockComponent({ qualifier: ComponentQualifier.File })], + page: 0, + total: 1 + }); + wrapper.instance().loadComponent(component.key); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot('with sub component'); +}); + +it('should refresh branch status if issues are updated', async () => { + const fetchBranchStatus = jest.fn(); + const branchLike = mockPullRequest(); + const wrapper = shallowRender({ branchLike, fetchBranchStatus }); + const instance = wrapper.instance(); + await waitAndUpdate(wrapper); + + instance.handleIssueChange(mockIssue()); + expect(fetchBranchStatus).toBeCalledWith(branchLike, 'foo'); +}); + +it('should load more behave correctly', async () => { + const component1 = mockComponent(); + const component2 = mockComponent(); + (retrieveComponent as jest.Mock).mockResolvedValueOnce({ + breadcrumbs: [], + component: mockComponent(), + components: [component1], + page: 0, + total: 1 + }); + let wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + (loadMoreChildren as jest.Mock).mockResolvedValueOnce({ + components: [component2], + page: 0, + total: 1 + }); + + wrapper.instance().handleLoadMore(); + expect(wrapper.state().components).toContainEqual(component1); + expect(wrapper.state().components).toContainEqual(component2); + + (retrieveComponent as jest.Mock).mockRejectedValueOnce({}); + wrapper = shallowRender(); + await waitAndUpdate(wrapper); + wrapper.instance().handleLoadMore(); + expect(wrapper.state().components).toBeUndefined(); +}); + +it('should handle go to parent correctly', async () => { + const router = mockRouter(); + (retrieveComponent as jest.Mock).mockResolvedValueOnce({ + breadcrumbs: [], + component: mockComponent(), + components: [], + page: 0, + total: 1 + }); + let wrapper = shallowRender(); + await waitAndUpdate(wrapper); + wrapper.instance().handleGoToParent(); + expect(wrapper.state().highlighted).toBeUndefined(); + + const breadcrumb = { key: 'key2', name: 'name2', qualifier: ComponentQualifier.Directory }; + (retrieveComponent as jest.Mock).mockResolvedValueOnce({ + breadcrumbs: [ + { key: 'key1', name: 'name1', qualifier: ComponentQualifier.Directory }, + breadcrumb + ], + component: mockComponent(), + components: [], + page: 0, + total: 1 + }); + wrapper = shallowRender({ router }); + await waitAndUpdate(wrapper); + wrapper.instance().handleGoToParent(); + expect(wrapper.state().highlighted).toBe(breadcrumb); + expect(router.push).toHaveBeenCalledWith({ + pathname: '/code', + query: { id: 'foo', line: undefined, selected: 'key1' } + }); +}); + +it('should handle select correctly', () => { + const router = mockRouter(); + const wrapper = shallowRender({ router }); + wrapper.setState({ highlighted: mockComponentMeasure() }); + + wrapper.instance().handleSelect(mockComponentMeasure(true, { refKey: 'test' })); + expect(router.push).toHaveBeenCalledWith({ + pathname: '/dashboard', + query: { branch: undefined, id: 'test' } + }); + expect(wrapper.state().highlighted).toBeUndefined(); + + wrapper.instance().handleSelect(mockComponentMeasure()); + expect(router.push).toHaveBeenCalledWith({ + pathname: '/dashboard', + query: { branch: undefined, id: 'test' } + }); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/App-test.tsx.snap deleted file mode 100644 index 7d2227bf3a1..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/App-test.tsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should have correct title for APP based component 1`] = ` - -`; - -exports[`should have correct title for portfolio base component 1`] = ` - -`; - -exports[`should have correct title for project component 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap new file mode 100644 index 00000000000..233b60a1e9f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/AppCode-test.tsx.snap @@ -0,0 +1,765 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correclty when no sub component for APP 1`] = ` +
+ + + +
+
+ + code_viewer.no_source_code_displayed_due_to_empty_analysis.APP + +
+
+
+`; + +exports[`should render correclty when no sub component for APP: no search 1`] = ` +
+ + + + +
+
+ +
+
+
+`; + +exports[`should render correclty when no sub component for APP: with sub component 1`] = ` +
+ + + + +
+
+ +
+ +
+
+`; + +exports[`should render correclty when no sub component for SVW 1`] = ` +
+ + + +
+
+ + code_viewer.no_source_code_displayed_due_to_empty_analysis.SVW + +
+
+
+`; + +exports[`should render correclty when no sub component for SVW: no search 1`] = ` +
+ + + + +
+
+ +
+
+
+`; + +exports[`should render correclty when no sub component for SVW: with sub component 1`] = ` +
+ + + + +
+
+ +
+ +
+
+`; + +exports[`should render correclty when no sub component for TRK 1`] = ` +
+ + + +
+
+ + code_viewer.no_source_code_displayed_due_to_empty_analysis.TRK + +
+
+
+`; + +exports[`should render correclty when no sub component for TRK: no search 1`] = ` +
+ + + + +
+
+ +
+
+
+`; + +exports[`should render correclty when no sub component for TRK: with sub component 1`] = ` +
+ + + + +
+
+ +
+ +
+
+`; + +exports[`should render correclty when no sub component for VW 1`] = ` +
+ + + +
+
+ + code_viewer.no_source_code_displayed_due_to_empty_analysis.VW + +
+
+
+`; + +exports[`should render correclty when no sub component for VW: no search 1`] = ` +
+ + + + +
+
+ +
+
+
+`; + +exports[`should render correclty when no sub component for VW: with sub component 1`] = ` +
+ + + + +
+
+ +
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/code/routes.ts b/server/sonar-web/src/main/js/apps/code/routes.ts index 854cd1ef120..14681d9db0c 100644 --- a/server/sonar-web/src/main/js/apps/code/routes.ts +++ b/server/sonar-web/src/main/js/apps/code/routes.ts @@ -21,7 +21,7 @@ import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent' const routes = [ { - indexRoute: { component: lazyLoadComponent(() => import('./components/App')) } + indexRoute: { component: lazyLoadComponent(() => import('./components/AppCode')) } } ]; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index f9ea058a2a3..ae92f530541 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1401,6 +1401,10 @@ duplications.dups_found_on_deleted_resource=This file contains duplicated blocks # GENERIC CODE VIEWER # #------------------------------------------------------------------------------ +code_viewer.no_source_code_displayed_due_to_empty_analysis.TRK=No code files were found for analysis. +code_viewer.no_source_code_displayed_due_to_empty_analysis.APP=No projects in this application. +code_viewer.no_source_code_displayed_due_to_empty_analysis.VW=No projects, applications, or portfolios in this portfolio. +code_viewer.no_source_code_displayed_due_to_empty_analysis.SVW=No projects, applications, or portfolios in this portfolio. code_viewer.no_source_code_displayed_due_to_security=Due to security settings, no source code can be displayed. code_viewer.no_source_code_displayed_due_to_source_removed=The file was removed, no source code can be displayed. -- 2.39.5