]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19728 Lock/unlock pages in the Project space while re-indexing
authorAmbroise C <ambroise.christea@sonarsource.com>
Tue, 18 Jul 2023 14:42:34 +0000 (16:42 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Jul 2023 20:03:04 +0000 (20:03 +0000)
server/sonar-web/design-system/src/components/NavBarTabs.tsx
server/sonar-web/src/main/js/api/project-management.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/ProjectCard.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx
server/sonar-web/src/main/js/apps/projects/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 17a2b2bff8877a21ff0589ddb2361522ad664c28..e3ef0ce068261c962c94750477435e5d1cc6dafc 100644 (file)
@@ -121,11 +121,12 @@ const NavBarTabLinkWrapper = styled.li`
     ${tw`sw-invisible`};
     content: attr(data-text);
   }
-  & > a.disabled-link,
-  & > a.disabled-link:hover,
-  & > a.disabled-link.hover,
-  & > a.disabled-link[aria-expanded='true'] {
-    ${tw`sw-cursor-default`};
+
+  &:has(a.disabled-link) > a,
+  &:has(a.disabled-link) > a:hover,
+  &:has(a.disabled-link) > a.hover,
+  &:has(a.disabled-link)[aria-expanded='true'] {
+    ${tw`sw-cursor-not-allowed`};
     border-bottom: ${themeBorder('xsActive', 'transparent', 1)};
     color: ${themeContrast('subnavigationDisabled')};
   }
index c12831d1ace20dfb0a28450d2d27d3f566c49f28..bda0c5b2eac40f93f6d26491439571f48b479a98 100644 (file)
@@ -51,18 +51,6 @@ export interface SearchProjectsParameters extends BaseSearchProjectsParameters {
   ps?: number;
 }
 
-export interface ComponentRaw {
-  key: string;
-  name: string;
-  isFavorite?: boolean;
-  analysisDate?: string;
-  qualifier: ComponentQualifier;
-  tags: string[];
-  visibility: Visibility;
-  leakPeriodDate?: string;
-  needIssueSync?: boolean;
-}
-
 export function getComponents(parameters: SearchProjectsParameters): Promise<{
   components: Project[];
   paging: Paging;
index d9c9c9e9864339d3fd09077fb67408c45f06957f..250f27e9a60f30b13167bbf520ccd2f700afc68a 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import { differenceBy } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
@@ -43,7 +44,6 @@ import withAvailableFeatures, {
   WithAvailableFeaturesProps,
 } from './available-features/withAvailableFeatures';
 import { ComponentContext } from './componentContext/ComponentContext';
-import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation';
 import ComponentNav from './nav/component/ComponentNav';
 
 interface Props extends WithAvailableFeaturesProps {
@@ -98,6 +98,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
         getComponentNavigation({ component: key, branch, pullRequest }),
         getComponentData({ component: key, branch, pullRequest }),
       ]);
+
       componentWithQualifier = this.addQualifier({ ...nav, ...component });
     } catch (e) {
       if (this.mounted) {
@@ -107,6 +108,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
           this.setState({ component: undefined, loading: false });
         }
       }
+
       return;
     }
 
@@ -123,6 +125,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     }
 
     let projectBinding;
+
     if (componentWithQualifier.qualifier === ComponentQualifier.Project) {
       projectBinding = await getProjectAlmBinding(key).catch(() => undefined);
     }
@@ -144,6 +147,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       ({ current, queue }) => {
         if (this.mounted) {
           let shouldFetchComponent = false;
+
           this.setState(
             ({ component, currentTask, tasksInProgress }) => {
               const newCurrentTask = this.getCurrentTask(current);
@@ -161,6 +165,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
               if (this.needsAnotherCheck(shouldFetchComponent, component, newTasksInProgress)) {
                 // Refresh the status as long as there is tasks in progress or no analysis
                 window.clearTimeout(this.watchStatusTimer);
+
                 this.watchStatusTimer = window.setTimeout(
                   () => this.fetchStatus(componentKey),
                   FETCH_STATUS_WAIT_TIME
@@ -168,6 +173,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
               }
 
               const isPending = pendingTasks.some((task) => task.status === TaskStatuses.Pending);
+
               return {
                 currentTask: newCurrentTask,
                 isPending,
@@ -195,6 +201,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       const projectBindingErrors = await validateProjectAlmBinding(component.key).catch(
         () => undefined
       );
+
       if (this.mounted) {
         this.setState({ projectBindingErrors });
       }
@@ -280,12 +287,15 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     if (!pullRequest && !branch) {
       return !task.branch && !task.pullRequest;
     }
+
     if (pullRequest) {
       return pullRequest === task.pullRequest;
     }
+
     if (branch) {
       return branch === task.branch;
     }
+
     return false;
   };
 
@@ -296,6 +306,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
           const newComponent: Component = { ...state.component, ...changes };
           return { component: newComponent };
         }
+
         return null;
       });
     }
@@ -308,12 +319,9 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       return <ComponentContainerNotFound />;
     }
 
-    if (component?.needIssueSync) {
-      return <PageUnavailableDueToIndexation component={component} />;
-    }
-
     const { currentTask, isPending, projectBinding, projectBindingErrors, tasksInProgress } =
       this.state;
+
     const isInProgress = tasksInProgress && tasksInProgress.length > 0;
 
     return (
@@ -325,6 +333,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
             component?.name ?? ''
           )}
         />
+
         {component &&
           !([ComponentQualifier.File, ComponentQualifier.TestFile] as string[]).includes(
             component.qualifier
@@ -338,6 +347,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
               projectBindingErrors={projectBindingErrors}
             />
           )}
+
         {loading ? (
           <div className="page page-limited">
             <i className="spinner" />
index 9faf1d9486b6d1c136275a1d15ef1bd7e4a82899..0ed48b25ac1d736383956248507fdb461998ecc0 100644 (file)
@@ -17,6 +17,7 @@
  * 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 { getProjectAlmBinding, validateProjectAlmBinding } from '../../../api/alm-settings';
@@ -37,12 +38,12 @@ import { TaskStatuses, TaskTypes } from '../../../types/tasks';
 import { Component } from '../../../types/types';
 import handleRequiredAuthorization from '../../utils/handleRequiredAuthorization';
 import { ComponentContainer } from '../ComponentContainer';
-import PageUnavailableDueToIndexation from '../indexation/PageUnavailableDueToIndexation';
 
 jest.mock('../../../api/branches', () => {
   const { mockMainBranch, mockPullRequest } = jest.requireActual(
     '../../../helpers/mocks/branch-like'
   );
+
   return {
     getBranches: jest
       .fn()
@@ -82,6 +83,7 @@ jest.mock('../../utils/handleRequiredAuthorization', () => ({
   default: jest.fn(),
 }));
 
+// eslint-disable-next-line react/function-component-definition
 const Inner = () => <div />;
 
 beforeEach(() => {
@@ -96,6 +98,7 @@ afterEach(() => {
 
 it('changes component', () => {
   const wrapper = shallowRender();
+
   wrapper.setState({
     component: {
       qualifier: ComponentQualifier.Project,
@@ -105,6 +108,7 @@ it('changes component', () => {
   });
 
   wrapper.instance().handleComponentChange({ visibility: Visibility.Private });
+
   expect(wrapper.state().component).toEqual({
     qualifier: ComponentQualifier.Project,
     visibility: Visibility.Private,
@@ -115,14 +119,25 @@ it('loads the project binding, if any', async () => {
   const component = mockComponent({
     breadcrumbs: [{ key: 'foo', name: 'foo', qualifier: ComponentQualifier.Project }],
   });
-  (getComponentNavigation as jest.Mock).mockResolvedValueOnce({});
-  (getComponentData as jest.Mock<any>)
-    .mockResolvedValueOnce({ component })
-    .mockResolvedValueOnce({ component });
-  (getProjectAlmBinding as jest.Mock).mockResolvedValueOnce(undefined).mockResolvedValueOnce({
-    alm: AlmKeys.GitHub,
-    key: 'foo',
-  });
+
+  jest
+    .mocked(getComponentNavigation)
+    .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
+
+  jest
+    .mocked(getComponentData)
+    .mockResolvedValueOnce({ component } as unknown as Awaited<ReturnType<typeof getComponentData>>)
+    .mockResolvedValueOnce({ component } as unknown as Awaited<
+      ReturnType<typeof getComponentData>
+    >);
+
+  jest
+    .mocked(getProjectAlmBinding)
+    .mockResolvedValueOnce(undefined as unknown as Awaited<ReturnType<typeof getProjectAlmBinding>>)
+    .mockResolvedValueOnce({
+      alm: AlmKeys.GitHub,
+      key: 'foo',
+    } as unknown as Awaited<ReturnType<typeof getProjectAlmBinding>>);
 
   const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
@@ -147,9 +162,9 @@ it("doesn't load branches portfolio", async () => {
 });
 
 it('fetches status', async () => {
-  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+  jest.mocked(getComponentData).mockResolvedValueOnce({
     component: {},
-  });
+  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
 
   const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
@@ -187,13 +202,15 @@ it('filters correctly the pending tasks for a main branch', () => {
 });
 
 it('reload component after task progress finished', async () => {
-  (getTasksForComponent as jest.Mock<any>)
+  jest
+    .mocked(getTasksForComponent)
     .mockResolvedValueOnce({
       queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.ViewRefresh }],
-    })
+    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>)
     .mockResolvedValueOnce({
       queue: [],
-    });
+    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+
   const wrapper = shallowRender();
 
   // First round, there's something in the queue, and component navigation was
@@ -226,15 +243,20 @@ it('reload component after task progress finished', async () => {
 });
 
 it('reloads component after task progress finished, and moves straight to current', async () => {
-  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+  jest.mocked(getComponentData).mockResolvedValueOnce({
     component: { key: 'bar' },
-  });
-  (getTasksForComponent as jest.Mock<any>)
-    .mockResolvedValueOnce({ queue: [] })
+  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
+
+  jest
+    .mocked(getTasksForComponent)
+    .mockResolvedValueOnce({ queue: [] } as unknown as Awaited<
+      ReturnType<typeof getTasksForComponent>
+    >)
     .mockResolvedValueOnce({
       queue: [],
       current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh },
-    });
+    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+
   const wrapper = shallowRender();
 
   // First round, nothing in the queue, and component navigation was not called
@@ -260,13 +282,15 @@ it('reloads component after task progress finished, and moves straight to curren
 });
 
 it('only fully loads a non-empty component once', async () => {
-  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+  jest.mocked(getComponentData).mockResolvedValueOnce({
     component: { key: 'bar', analysisDate: '2019-01-01' },
-  });
-  (getTasksForComponent as jest.Mock<any>).mockResolvedValueOnce({
+  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
+
+  jest.mocked(getTasksForComponent).mockResolvedValueOnce({
     queue: [],
     current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.Report },
-  });
+  } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+
   const wrapper = shallowRender();
 
   await waitAndUpdate(wrapper);
@@ -275,17 +299,20 @@ it('only fully loads a non-empty component once', async () => {
 });
 
 it('only fully reloads a non-empty component if there was previously some task in progress', async () => {
-  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+  jest.mocked(getComponentData).mockResolvedValueOnce({
     component: { key: 'bar', analysisDate: '2019-01-01' },
-  });
-  (getTasksForComponent as jest.Mock<any>)
+  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
+
+  jest
+    .mocked(getTasksForComponent)
     .mockResolvedValueOnce({
       queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.AppRefresh }],
-    })
+    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>)
     .mockResolvedValueOnce({
       queue: [],
       current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh },
-    });
+    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+
   const wrapper = shallowRender();
 
   // First round, a pending task in the queue. This should trigger a reload of the
@@ -309,18 +336,20 @@ it('only fully reloads a non-empty component if there was previously some task i
 });
 
 it('should show component not found if it does not exist', async () => {
-  (getComponentNavigation as jest.Mock).mockRejectedValueOnce(
-    new Response(null, { status: HttpStatus.NotFound })
-  );
+  jest
+    .mocked(getComponentNavigation)
+    .mockRejectedValueOnce(new Response(null, { status: HttpStatus.NotFound }));
+
   const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
 it('should redirect if the user has no access', async () => {
-  (getComponentNavigation as jest.Mock).mockRejectedValueOnce(
-    new Response(null, { status: HttpStatus.Forbidden })
-  );
+  jest
+    .mocked(getComponentNavigation)
+    .mockRejectedValueOnce(new Response(null, { status: HttpStatus.Forbidden }));
+
   const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
   expect(handleRequiredAuthorization).toHaveBeenCalled();
@@ -328,9 +357,10 @@ it('should redirect if the user has no access', async () => {
 
 it('should redirect if the component is a portfolio', async () => {
   const componentKey = 'comp-key';
-  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+
+  jest.mocked(getComponentData).mockResolvedValueOnce({
     component: { key: componentKey, breadcrumbs: [{ qualifier: ComponentQualifier.Portfolio }] },
-  });
+  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
 
   const replace = jest.fn();
 
@@ -338,20 +368,9 @@ it('should redirect if the component is a portfolio', async () => {
     location: mockLocation({ pathname: '/dashboard' }),
     router: mockRouter({ replace }),
   });
-  await waitAndUpdate(wrapper);
-  expect(replace).toHaveBeenCalledWith({ pathname: '/portfolio', search: `?id=${componentKey}` });
-});
-
-it('should display display the unavailable page if the component needs issue sync', async () => {
-  (getComponentData as jest.Mock).mockResolvedValueOnce({
-    component: { key: 'test', qualifier: ComponentQualifier.Project, needIssueSync: true },
-  });
-
-  const wrapper = shallowRender();
 
   await waitAndUpdate(wrapper);
-
-  expect(wrapper.find(PageUnavailableDueToIndexation).exists()).toBe(true);
+  expect(replace).toHaveBeenCalledWith({ pathname: '/portfolio', search: `?id=${componentKey}` });
 });
 
 describe('should correctly validate the project binding depending on the context', () => {
@@ -365,11 +384,18 @@ describe('should correctly validate the project binding depending on the context
     ['has a project binding; check is OK', COMPONENT, undefined, 1],
     ['has a project binding; check is not OK', COMPONENT, PROJECT_BINDING_ERRORS, 1],
   ])('%s', async (_, component, projectBindingErrors = undefined, n = 0) => {
-    (getComponentNavigation as jest.Mock).mockResolvedValueOnce({});
-    (getComponentData as jest.Mock<any>).mockResolvedValueOnce({ component });
+    jest
+      .mocked(getComponentNavigation)
+      .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
+
+    jest
+      .mocked(getComponentData)
+      .mockResolvedValueOnce({ component } as unknown as Awaited<
+        ReturnType<typeof getComponentData>
+      >);
 
     if (n > 0) {
-      (validateProjectAlmBinding as jest.Mock).mockResolvedValueOnce(projectBindingErrors);
+      jest.mocked(validateProjectAlmBinding).mockResolvedValueOnce(projectBindingErrors);
     }
 
     const wrapper = shallowRender({ hasFeature: () => true });
@@ -390,8 +416,16 @@ it.each([
     const component = mockComponent({
       breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: componentQualifier }],
     });
-    (getComponentNavigation as jest.Mock).mockResolvedValueOnce({});
-    (getComponentData as jest.Mock<any>).mockResolvedValueOnce({ component });
+
+    jest
+      .mocked(getComponentNavigation)
+      .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
+
+    jest
+      .mocked(getComponentData)
+      .mockResolvedValueOnce({ component } as unknown as Awaited<
+        ReturnType<typeof getComponentData>
+      >);
 
     const wrapper = shallowRender();
     await waitAndUpdate(wrapper);
index 519288d19a57cb168c5482bcdca785bba77375d0..c3e7ac009fb41256fbc3fe36080562b011d70958 100644 (file)
@@ -63,17 +63,7 @@ function renderFirstLine(
   handleFavorite: Props['handleFavorite'],
   isNewCode: boolean
 ) {
-  const {
-    analysisDate,
-    tags,
-    qualifier,
-    isFavorite,
-    needIssueSync,
-    key,
-    name,
-    measures,
-    visibility,
-  } = project;
+  const { analysisDate, isFavorite, key, measures, name, qualifier, tags, visibility } = project;
   const formatted = formatMeasure(measures[MetricKey.alert_status], MetricType.Level);
   const qualityGateLabel = translateWithParameters('overview.quality_gate_x', formatted);
   return (
@@ -92,7 +82,7 @@ function renderFirstLine(
           )}
 
           <h3 className="it__project-card-name" title={name}>
-            {needIssueSync ? name : <StandoutLink to={getProjectUrl(key)}>{name}</StandoutLink>}
+            <StandoutLink to={getProjectUrl(key)}>{name}</StandoutLink>
           </h3>
 
           {qualifier === ComponentQualifier.Application && (
@@ -211,7 +201,7 @@ function renderSecondLine(
   project: Props['project'],
   isNewCode: boolean
 ) {
-  const { measures, analysisDate, needIssueSync, leakPeriodDate, qualifier, key } = project;
+  const { analysisDate, key, leakPeriodDate, measures, qualifier } = project;
 
   if (analysisDate && (!isNewCode || leakPeriodDate)) {
     return (
@@ -230,14 +220,11 @@ function renderSecondLine(
           ? translate('projects.no_new_code_period', qualifier)
           : translate('projects.not_analyzed', qualifier)}
       </Note>
-      {qualifier !== ComponentQualifier.Application &&
-        !analysisDate &&
-        isLoggedIn(currentUser) &&
-        !needIssueSync && (
-          <StandoutLink className="sw-ml-2 sw-body-sm-highlight" to={getProjectUrl(key)}>
-            {translate('projects.configure_analysis')}
-          </StandoutLink>
-        )}
+      {qualifier !== ComponentQualifier.Application && !analysisDate && isLoggedIn(currentUser) && (
+        <StandoutLink className="sw-ml-2 sw-body-sm-highlight" to={getProjectUrl(key)}>
+          {translate('projects.configure_analysis')}
+        </StandoutLink>
+      )}
     </div>
   );
 }
@@ -249,12 +236,8 @@ export default function ProjectCard(props: Props) {
   return (
     <ProjectCardWrapper
       className={classNames(
-        'it_project_card sw-relative sw-box-border sw-rounded-1 sw-mb-page sw-h-full',
-        {
-          'project-card-disabled': project.needIssueSync,
-        }
+        'it_project_card sw-relative sw-box-border sw-rounded-1 sw-mb-page sw-h-full'
       )}
-      aria-disabled={project.needIssueSync}
       data-key={project.key}
     >
       {renderFirstLine(project, props.handleFavorite, isNewCode)}
index 77d82ca616284d07724524644fa25e5bc49f1bcd..b1da5b7ce96f25b52d23057f6f18910c4e450e28 100644 (file)
@@ -46,12 +46,6 @@ const PROJECT: Project = {
 const USER_LOGGED_OUT = mockCurrentUser();
 const USER_LOGGED_IN = mockLoggedInUser();
 
-it('should display correclty when project need issue sync and not setup', () => {
-  renderProjectCard({ ...PROJECT, needIssueSync: true });
-  expect(screen.getByLabelText('overview.quality_gate_x.OK')).toBeInTheDocument();
-  expect(screen.getByText('overview.project.main_branch_empty')).toBeInTheDocument();
-});
-
 it('should not display the quality gate', () => {
   const project = { ...PROJECT, analysisDate: undefined };
   renderProjectCard(project);
index a56e9d81e00d6b1dd426a1a83fff6f6e90d092c6..e31c013859a89c2c396a5d40b724791196bf2973 100644 (file)
@@ -31,7 +31,6 @@ export interface Project {
   qualifier: ComponentQualifier;
   tags: string[];
   visibility: Visibility;
-  needIssueSync?: boolean;
 }
 
 export interface Facet {
index 045649235387f7c7f40cce3dd5223950ccc4e647..62c85b682afe0042cfdaf2e27a16ccee49318e30 100644 (file)
@@ -544,6 +544,7 @@ 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 your project has been reindexed.
 
 sidebar.projects=Projects
 sidebar.project_settings=Configuration