From 3b618651aa506286bcae73648408f0a787ba2f4f Mon Sep 17 00:00:00 2001 From: David Cho-Lerat Date: Tue, 18 Jul 2023 16:43:51 +0200 Subject: [PATCH] SONAR-19777 Lock project security reports page --- .../src/components/NavBarTabs.tsx | 4 +- .../PageUnavailableDueToIndexation.tsx | 61 +++---- .../PageUnavailableDueToIndexation-test.tsx | 12 +- ...geUnavailableDueToIndexation-test.tsx.snap | 49 +++--- .../js/apps/issues/components/IssuesApp.tsx | 159 +++++++++++------- .../__tests__/withIndexationGuard-test.tsx | 3 +- .../js/components/hoc/withIndexationGuard.tsx | 36 ++-- .../resources/org/sonar/l10n/core.properties | 5 +- 8 files changed, 167 insertions(+), 162 deletions(-) diff --git a/server/sonar-web/design-system/src/components/NavBarTabs.tsx b/server/sonar-web/design-system/src/components/NavBarTabs.tsx index e3ef0ce0682..3de2b0b64f0 100644 --- a/server/sonar-web/design-system/src/components/NavBarTabs.tsx +++ b/server/sonar-web/design-system/src/components/NavBarTabs.tsx @@ -24,9 +24,9 @@ import React from 'react'; import tw, { theme } from 'twin.macro'; import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; import { isDefined } from '../helpers/types'; +import { ChevronDownIcon } from './icons/ChevronDownIcon'; import NavLink, { NavLinkProps } from './NavLink'; import Tooltip from './Tooltip'; -import { ChevronDownIcon } from './icons/ChevronDownIcon'; interface Props extends React.HTMLAttributes { children?: React.ReactNode; @@ -126,7 +126,7 @@ const NavBarTabLinkWrapper = styled.li` &:has(a.disabled-link) > a:hover, &:has(a.disabled-link) > a.hover, &:has(a.disabled-link)[aria-expanded='true'] { - ${tw`sw-cursor-not-allowed`}; + ${tw`sw-cursor-default`}; border-bottom: ${themeBorder('xsActive', 'transparent', 1)}; color: ${themeContrast('subnavigationDisabled')}; } diff --git a/server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx b/server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx index 8bbd4dbc43b..eead7093fcc 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx @@ -17,26 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { FlagMessage, Link } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import withIndexationContext, { WithIndexationContextProps, } from '../../../components/hoc/withIndexationContext'; -import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; -import { Component } from '../../../types/types'; - -interface Props extends WithIndexationContextProps { - pageContext?: PageContext; - component?: Pick; -} - -export enum PageContext { - Issues = 'issues', - Portfolios = 'portfolios', -} -export class PageUnavailableDueToIndexation extends React.PureComponent { +export class PageUnavailableDueToIndexation extends React.PureComponent { componentDidUpdate() { if ( this.props.indexationContext.status.isCompleted && @@ -47,33 +37,28 @@ export class PageUnavailableDueToIndexation extends React.PureComponent { } render() { - const { pageContext, component } = this.props; - let messageKey = 'indexation.page_unavailable.title'; - - if (pageContext) { - messageKey = `${messageKey}.${pageContext}`; - } - return (
-
-

- {component?.name}, - }} - /> -

- -

{translate('indexation.page_unavailable.description')}

-

- {translate('indexation.page_unavailable.description.additional_information')} -

-
-
+ + {translate('indexation.page_unavailable.description')} +
+ + {translate('learn_more')} + + ), + }} + /> +
); } diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx index b4eecca76f1..543fbdd4519 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx @@ -17,13 +17,14 @@ * 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 { ComponentQualifier } from '../../../../types/component'; -import { PageContext, PageUnavailableDueToIndexation } from '../PageUnavailableDueToIndexation'; +import { PageUnavailableDueToIndexation } from '../PageUnavailableDueToIndexation'; it('should render correctly', () => { const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); }); @@ -42,6 +43,7 @@ it('should not refresh the page once the indexation is complete if there were fa wrapper.setProps({ indexationContext: { status: { isCompleted: true, percentCompleted: 100, hasFailures: true } }, }); + wrapper.update(); expect(reload).not.toHaveBeenCalled(); @@ -62,20 +64,18 @@ it('should refresh the page once the indexation is complete if there were NO fai wrapper.setProps({ indexationContext: { status: { isCompleted: true, percentCompleted: 100, hasFailures: false } }, }); + wrapper.update(); expect(reload).toHaveBeenCalled(); }); -function shallowRender(props?: PageUnavailableDueToIndexation['props']) { +function shallowRender() { return shallow( ); } diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap index e3422fb9aa4..43fa2200495 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap @@ -4,37 +4,26 @@ exports[`should render correctly 1`] = `
-
-

- - test-portfolio - , - "componentQualifier": "qualifier.VW", - } + indexation.page_unavailable.description +
+ + learn_more + , } - /> -

- -

- indexation.page_unavailable.description -

-

- indexation.page_unavailable.description.additional_information -

-
-
+ } + /> +
`; diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index 3d38005558c..5eced96ea2e 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -35,16 +35,18 @@ import { keyBy, omit, without } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import { searchIssues } from '../../../api/issues'; +import { listIssues, searchIssues } from '../../../api/issues'; import { getRuleDetails } from '../../../api/rules'; import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; -import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import EmptySearch from '../../../components/common/EmptySearch'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import ListFooter from '../../../components/controls/ListFooter'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; +import withIndexationContext, { + WithIndexationContextProps, +} from '../../../components/hoc/withIndexationContext'; import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import IssueTabViewer from '../../../components/rules/IssueTabViewer'; @@ -106,7 +108,7 @@ import NoMyIssues from './NoMyIssues'; import PageActions from './PageActions'; import StyledHeader, { PSEUDO_SHADOW_HEIGHT } from './StyledHeader'; -interface Props { +interface Props extends WithIndexationContextProps { branchLike?: BranchLike; component?: Component; currentUser: CurrentUser; @@ -147,6 +149,7 @@ export interface State { const DEFAULT_QUERY = { resolved: 'false' }; const MAX_INITAL_FETCH = 1000; const VARIANTS_FACET = 'codeVariants'; +const ISSUES_PAGE_SIZE = 100; export class App extends React.PureComponent { mounted = false; @@ -456,6 +459,19 @@ export class App extends React.PureComponent { createdAfterIncludesTime = () => Boolean(this.props.location.query.createdAfter?.includes('T')); fetchIssuesHelper = (query: RawQuery) => { + if (this.props.component?.needIssueSync) { + return listIssues({ + ...query, + }).then((response) => { + const { components, issues, rules } = response; + const parsedIssues = issues.map((issue) => + parseIssueFromResponse(issue, components, undefined, rules) + ); + + return { ...response, issues: parsedIssues } as FetchIssuesPromise; + }); + } + return searchIssues({ ...query, additionalFields: '_all', @@ -487,15 +503,23 @@ export class App extends React.PureComponent { facets = facets ? `${facets},${VARIANTS_FACET}` : VARIANTS_FACET; } - const parameters: Dict = { - ...getBranchLikeQuery(this.props.branchLike), - componentKeys: component?.key, - s: 'FILE_LINE', - ...serializeQuery(query), - ps: '100', - facets, - ...additional, - }; + const parameters: Dict = component?.needIssueSync + ? { + ...getBranchLikeQuery(this.props.branchLike, true), + project: component?.key, + ...serializeQuery(query), + ps: `${ISSUES_PAGE_SIZE}`, + ...additional, + } + : { + ...getBranchLikeQuery(this.props.branchLike), + componentKeys: component?.key, + s: 'FILE_LINE', + ...serializeQuery(query), + ps: `${ISSUES_PAGE_SIZE}`, + facets, + ...additional, + }; if (query.createdAfter !== undefined && this.createdAfterIncludesTime()) { parameters.createdAfter = serializeDate(query.createdAfter); @@ -535,50 +559,51 @@ export class App extends React.PureComponent { fetchPromise = this.fetchIssues({}, true, firstRequest); } - return fetchPromise.then( - ({ effortTotal, facets, issues, paging, ...other }) => { - if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) { - const openIssue = getOpenIssue(this.props, issues); - let selected: string | undefined = undefined; - - if (issues.length > 0) { - selected = openIssue ? openIssue.key : issues[0].key; - } - - this.setState(({ showVariantsFilter }) => ({ - cannotShowOpenIssue: Boolean(openIssueKey && !openIssue), - effortTotal, - facets: parseFacets(facets), - showVariantsFilter: firstRequest - ? Boolean(facets.find((f) => f.property === VARIANTS_FACET)?.values.length) - : showVariantsFilter, - loading: false, - locationsNavigator: true, - issues, - openIssue, - paging, - referencedComponentsById: keyBy(other.components, 'uuid'), - referencedComponentsByKey: keyBy(other.components, 'key'), - referencedLanguages: keyBy(other.languages, 'key'), - referencedRules: keyBy(other.rules, 'key'), - referencedUsers: keyBy(other.users, 'login'), - selected, - selectedFlowIndex: 0, - selectedLocationIndex: undefined, - })); - } + return fetchPromise.then(this.parseFirstIssues(firstRequest, openIssueKey, prevQuery), () => { + if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) { + this.setState({ loading: false }); + } - return issues; - }, - () => { - if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) { - this.setState({ loading: false }); + return []; + }); + } + + parseFirstIssues = + (firstRequest: boolean, openIssueKey: string | undefined, prevQuery: RawQuery) => + ({ effortTotal, facets, issues, paging, ...other }: FetchIssuesPromise) => { + if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) { + const openIssue = getOpenIssue(this.props, issues); + let selected: string | undefined = undefined; + + if (issues.length > 0) { + selected = openIssue ? openIssue.key : issues[0].key; } - return []; + this.setState(({ showVariantsFilter }) => ({ + cannotShowOpenIssue: Boolean(openIssueKey && !openIssue), + effortTotal, + facets: parseFacets(facets), + showVariantsFilter: firstRequest + ? Boolean(facets?.find((f) => f.property === VARIANTS_FACET)?.values.length) + : showVariantsFilter, + loading: false, + locationsNavigator: true, + issues, + openIssue, + paging, + referencedComponentsById: keyBy(other.components, 'uuid'), + referencedComponentsByKey: keyBy(other.components, 'key'), + referencedLanguages: keyBy(other.languages, 'key'), + referencedRules: keyBy(other.rules, 'key'), + referencedUsers: keyBy(other.users, 'login'), + selected, + selectedFlowIndex: 0, + selectedLocationIndex: undefined, + })); } - ); - } + + return issues; + }; fetchIssuesPage = (p: number) => { return this.fetchIssues({ p }); @@ -966,7 +991,7 @@ export class App extends React.PureComponent { > {warning &&
{warning}
} - {currentUser.isLoggedIn && ( + {currentUser.isLoggedIn && !component?.needIssueSync && (
{ let noIssuesMessage = null; - if (paging.total === 0 && !loading) { + if (issues.length === 0 && !loading) { if (this.isFiltered()) { noIssuesMessage = ; } else if (this.state.myIssues) { @@ -1103,7 +1128,7 @@ export class App extends React.PureComponent {

{translate('list_of_issues')}

- {paging.total > 0 && ( + {issues.length > 0 && ( { /> )} - {paging.total > 0 && ( + {issues.length > 0 && ( { this.fetchMoreIssues().catch(() => undefined); }} loading={loadingMore} + pageSize={ISSUES_PAGE_SIZE} total={paging.total} useMIUIButtons /> @@ -1158,7 +1184,7 @@ export class App extends React.PureComponent {
@@ -1302,9 +1328,22 @@ export class App extends React.PureComponent { } } -export default withIndexationGuard( - withRouter(withComponentContext(withCurrentUserContext(withBranchLikes(App)))), - PageContext.Issues +export default withRouter( + withComponentContext( + withCurrentUserContext( + withBranchLikes( + withIndexationContext( + withIndexationGuard({ + Component: App, + showIndexationMessage: ({ component, indexationContext }) => + (!component && indexationContext.status.isCompleted === false) || + (component?.qualifier !== ComponentQualifier.Project && + component?.needIssueSync === true), + }) + ) + ) + ) + ) ); const PageWrapperStyle = styled.div` diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx index ae691a9176d..39cab4e402e 100644 --- a/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx +++ b/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx @@ -20,7 +20,6 @@ import { mount } from 'enzyme'; import * as React from 'react'; import { IndexationContext } from '../../../app/components/indexation/IndexationContext'; -import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation'; import { IndexationContextInterface } from '../../../types/indexation'; import withIndexationGuard from '../withIndexationGuard'; @@ -62,4 +61,4 @@ class TestComponent extends React.PureComponent { } } -const TestComponentWithGuard = withIndexationGuard(TestComponent, PageContext.Issues); +const TestComponentWithGuard = withIndexationGuard(TestComponent); diff --git a/server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx b/server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx index d30d1a69f7d..b70ee49246c 100644 --- a/server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx +++ b/server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx @@ -17,29 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; import { IndexationContext } from '../../app/components/indexation/IndexationContext'; -import PageUnavailableDueToIndexation, { - PageContext, -} from '../../app/components/indexation/PageUnavailableDueToIndexation'; +import PageUnavailableDueToIndexation from '../../app/components/indexation/PageUnavailableDueToIndexation'; -export default function withIndexationGuard

( - WrappedComponent: React.ComponentType

, - pageContext: PageContext -) { - return class WithIndexationGuard extends React.PureComponent

{ - render() { - return ( - - {(context) => - context?.status.isCompleted && !context?.status.hasFailures ? ( - - ) : ( - - ) - } - - ); - } +export default function withIndexationGuard

(WrappedComponent: React.ComponentType

) { + return function WithIndexationGuard(props: React.PropsWithChildren

) { + return ( + + {(context) => + context?.status.isCompleted && !context?.status.hasFailures ? ( + + ) : ( + + ) + } + + ); }; } 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 971d1ebe226..55d28157982 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -544,7 +544,6 @@ layout.security_reports=Security Reports layout.nav.home_logo_alt=Logo, link to homepage layout.must_be_configured=This will be available once your project is configured and analyzed. layout.all_project_must_be_accessible=You need access to all projects within this {0} to access it. -layout.component_must_be_reindexed=This will be available once the reindexing has completed. sidebar.projects=Projects sidebar.project_settings=Configuration @@ -4734,8 +4733,8 @@ indexation.admin_link=See {link} for more information. indexation.page_unavailable.title.issues=Issues page is temporarily unavailable indexation.page_unavailable.title.portfolios=Portfolios page is temporarily unavailable indexation.page_unavailable.title={componentQualifier} {componentName} is temporarily unavailable -indexation.page_unavailable.description=This page will be available after the data is reloaded. This might take a while depending on the amount of projects and issues in your SonarQube instance. -indexation.page_unavailable.description.additional_information=You can keep analyzing your projects during this process. +indexation.page_unavailable.description=SonarQube is reindexing project data. +indexation.page_unavailable.description.additional_information=This page is unavailable until this process is complete. {link} #------------------------------------------------------------------------------ -- 2.39.5