]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19777 Lock project security reports page
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Tue, 18 Jul 2023 14:43:51 +0000 (16:43 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Jul 2023 20:03:05 +0000 (20:03 +0000)
server/sonar-web/design-system/src/components/NavBarTabs.tsx
server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx
server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index e3ef0ce068261c962c94750477435e5d1cc6dafc..3de2b0b64f0a4c5e4ff85a40692f7da61ee7e549 100644 (file)
@@ -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<HTMLUListElement> {
   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')};
   }
index 8bbd4dbc43b97b9f5a57182a0386a17729846536..eead7093fcc4e9f2277238d491dc9db3738ce017 100644 (file)
  * 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<Component, 'qualifier' | 'name'>;
-}
-
-export enum PageContext {
-  Issues = 'issues',
-  Portfolios = 'portfolios',
-}
 
-export class PageUnavailableDueToIndexation extends React.PureComponent<Props> {
+export class PageUnavailableDueToIndexation extends React.PureComponent<WithIndexationContextProps> {
   componentDidUpdate() {
     if (
       this.props.indexationContext.status.isCompleted &&
@@ -47,33 +37,28 @@ export class PageUnavailableDueToIndexation extends React.PureComponent<Props> {
   }
 
   render() {
-    const { pageContext, component } = this.props;
-    let messageKey = 'indexation.page_unavailable.title';
-
-    if (pageContext) {
-      messageKey = `${messageKey}.${pageContext}`;
-    }
-
     return (
       <div className="page-wrapper-simple">
-        <div className="page-simple">
-          <h1 className="big-spacer-bottom">
-            <FormattedMessage
-              id={messageKey}
-              defaultMessage={translate(messageKey)}
-              values={{
-                componentQualifier: translate('qualifier', component?.qualifier ?? ''),
-                componentName: <em>{component?.name}</em>,
-              }}
-            />
-          </h1>
-          <Alert variant="info">
-            <p>{translate('indexation.page_unavailable.description')}</p>
-            <p className="spacer-top">
-              {translate('indexation.page_unavailable.description.additional_information')}
-            </p>
-          </Alert>
-        </div>
+        <FlagMessage className="sw-m-10" variant="info">
+          {translate('indexation.page_unavailable.description')}
+          <br />
+          <FormattedMessage
+            defaultMessage={translate(
+              'indexation.page_unavailable.description.additional_information'
+            )}
+            id="indexation.page_unavailable.description.additional_information"
+            values={{
+              link: (
+                <Link
+                  className="sw-ml-4"
+                  to="https://docs.sonarqube.org/latest/instance-administration/reindexing/"
+                >
+                  {translate('learn_more')}
+                </Link>
+              ),
+            }}
+          />
+        </FlagMessage>
       </div>
     );
   }
index b4eecca76f1d3e5bd54de45cd8a60a699284592a..543fbdd4519c5b9e334b4bebc281a29309f144a9 100644 (file)
  * 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<PageUnavailableDueToIndexation>(
     <PageUnavailableDueToIndexation
       indexationContext={{
         status: { isCompleted: false, percentCompleted: 23, hasFailures: false },
       }}
-      pageContext={PageContext.Issues}
-      component={{ qualifier: ComponentQualifier.Portfolio, name: 'test-portfolio' }}
-      {...props}
     />
   );
 }
index e3422fb9aa4fc841154282b5ac33f6442b23013b..43fa2200495a80503d8a904eb80b8f142ef2cc9d 100644 (file)
@@ -4,37 +4,26 @@ exports[`should render correctly 1`] = `
 <div
   className="page-wrapper-simple"
 >
-  <div
-    className="page-simple"
+  <FlagMessage
+    className="sw-m-10"
+    variant="info"
   >
-    <h1
-      className="big-spacer-bottom"
-    >
-      <FormattedMessage
-        defaultMessage="indexation.page_unavailable.title.issues"
-        id="indexation.page_unavailable.title.issues"
-        values={
-          {
-            "componentName": <em>
-              test-portfolio
-            </em>,
-            "componentQualifier": "qualifier.VW",
-          }
+    indexation.page_unavailable.description
+    <br />
+    <FormattedMessage
+      defaultMessage="indexation.page_unavailable.description.additional_information"
+      id="indexation.page_unavailable.description.additional_information"
+      values={
+        {
+          "link": <StandoutLink
+            className="sw-ml-4"
+            to="https://docs.sonarqube.org/latest/instance-administration/reindexing/"
+          >
+            learn_more
+          </StandoutLink>,
         }
-      />
-    </h1>
-    <Alert
-      variant="info"
-    >
-      <p>
-        indexation.page_unavailable.description
-      </p>
-      <p
-        className="spacer-top"
-      >
-        indexation.page_unavailable.description.additional_information
-      </p>
-    </Alert>
-  </div>
+      }
+    />
+  </FlagMessage>
 </div>
 `;
index 3d38005558cca28970f152c8ba8e5f04ee61c9e7..5eced96ea2e646afe40b947cf980a94e02a879d9 100644 (file)
@@ -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<Props, State> {
   mounted = false;
@@ -456,6 +459,19 @@ export class App extends React.PureComponent<Props, State> {
   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<Props, State> {
       facets = facets ? `${facets},${VARIANTS_FACET}` : VARIANTS_FACET;
     }
 
-    const parameters: Dict<string | undefined> = {
-      ...getBranchLikeQuery(this.props.branchLike),
-      componentKeys: component?.key,
-      s: 'FILE_LINE',
-      ...serializeQuery(query),
-      ps: '100',
-      facets,
-      ...additional,
-    };
+    const parameters: Dict<string | undefined> = 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<Props, State> {
       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<Props, State> {
       >
         {warning && <div className="sw-pb-6">{warning}</div>}
 
-        {currentUser.isLoggedIn && (
+        {currentUser.isLoggedIn && !component?.needIssueSync && (
           <div className="sw-flex sw-justify-start sw-mb-8">
             <ToggleButton
               onChange={this.handleMyIssuesChange}
@@ -1089,7 +1114,7 @@ export class App extends React.PureComponent<Props, State> {
 
     let noIssuesMessage = null;
 
-    if (paging.total === 0 && !loading) {
+    if (issues.length === 0 && !loading) {
       if (this.isFiltered()) {
         noIssuesMessage = <EmptySearch />;
       } else if (this.state.myIssues) {
@@ -1103,7 +1128,7 @@ export class App extends React.PureComponent<Props, State> {
       <div>
         <h2 className="a11y-hidden">{translate('list_of_issues')}</h2>
 
-        {paging.total > 0 && (
+        {issues.length > 0 && (
           <IssuesList
             branchLike={branchLike}
             checked={this.state.checked}
@@ -1120,13 +1145,14 @@ export class App extends React.PureComponent<Props, State> {
           />
         )}
 
-        {paging.total > 0 && (
+        {issues.length > 0 && (
           <ListFooter
             count={issues.length}
             loadMore={() => {
               this.fetchMoreIssues().catch(() => undefined);
             }}
             loading={loadingMore}
+            pageSize={ISSUES_PAGE_SIZE}
             total={paging.total}
             useMIUIButtons
           />
@@ -1158,7 +1184,7 @@ export class App extends React.PureComponent<Props, State> {
             <PageActions
               canSetHome={!this.props.component}
               effortTotal={this.state.effortTotal}
-              paging={paging}
+              paging={this.props.component?.needIssueSync ? undefined : paging}
               selectedIndex={selectedIndex}
             />
           </div>
@@ -1302,9 +1328,22 @@ export class App extends React.PureComponent<Props, State> {
   }
 }
 
-export default withIndexationGuard(
-  withRouter(withComponentContext(withCurrentUserContext(withBranchLikes(App)))),
-  PageContext.Issues
+export default withRouter(
+  withComponentContext(
+    withCurrentUserContext(
+      withBranchLikes(
+        withIndexationContext(
+          withIndexationGuard<Props & WithIndexationContextProps>({
+            Component: App,
+            showIndexationMessage: ({ component, indexationContext }) =>
+              (!component && indexationContext.status.isCompleted === false) ||
+              (component?.qualifier !== ComponentQualifier.Project &&
+                component?.needIssueSync === true),
+          })
+        )
+      )
+    )
+  )
 );
 
 const PageWrapperStyle = styled.div`
index ae691a9176d86275b2c71f268ae310408d65d3ef..39cab4e402e4b918ddace7d84a49184f2e3b6b0c 100644 (file)
@@ -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);
index d30d1a69f7dd5ed65ac5a6152364d1ab2ba5bdec..b70ee49246c5edc09469fdc183446dce200016de 100644 (file)
  * 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<P>(
-  WrappedComponent: React.ComponentType<P>,
-  pageContext: PageContext
-) {
-  return class WithIndexationGuard extends React.PureComponent<P> {
-    render() {
-      return (
-        <IndexationContext.Consumer>
-          {(context) =>
-            context?.status.isCompleted && !context?.status.hasFailures ? (
-              <WrappedComponent {...this.props} />
-            ) : (
-              <PageUnavailableDueToIndexation pageContext={pageContext} />
-            )
-          }
-        </IndexationContext.Consumer>
-      );
-    }
+export default function withIndexationGuard<P>(WrappedComponent: React.ComponentType<P>) {
+  return function WithIndexationGuard(props: React.PropsWithChildren<P>) {
+    return (
+      <IndexationContext.Consumer>
+        {(context) =>
+          context?.status.isCompleted && !context?.status.hasFailures ? (
+            <WrappedComponent {...props} />
+          ) : (
+            <PageUnavailableDueToIndexation />
+          )
+        }
+      </IndexationContext.Consumer>
+    );
   };
 }
index 971d1ebe226019d38bd338f0362fef14be378b4d..55d2815798201c0153edf86f1fe0f997e547614f 100644 (file)
@@ -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}
 
 
 #------------------------------------------------------------------------------