]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21467 Implement new branch overview header
authorstanislavh <stanislav.honcharov@sonarsource.com>
Mon, 22 Jan 2024 16:52:24 +0000 (17:52 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 24 Jan 2024 20:03:32 +0000 (20:03 +0000)
42 files changed:
server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/componentContext/withComponentContext.tsx
server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx
server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts [deleted file]
server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx [deleted file]
server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
server/sonar-web/src/main/js/components/controls/__tests__/HomePageSelect-test.tsx
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/types/component.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index e5746f7ffae505e919c809f20a0eebd238870034..71abfecd79d69e00f710ac9758d5677cf9286bee 100644 (file)
@@ -23,11 +23,18 @@ import { PAGE_SIZE } from '../../apps/background-tasks/constants';
 import { parseDate } from '../../helpers/dates';
 import { mockTask } from '../../helpers/mocks/tasks';
 import { isDefined } from '../../helpers/types';
-import { ActivityRequestParameters, Task, TaskStatuses, TaskTypes } from '../../types/tasks';
+import {
+  ActivityRequestParameters,
+  Task,
+  TaskStatuses,
+  TaskTypes,
+  TaskWarning,
+} from '../../types/tasks';
 import {
   cancelAllTasks,
   cancelTask,
   getActivity,
+  getAnalysisStatus,
   getStatus,
   getTask,
   getTasksForComponent,
@@ -63,6 +70,7 @@ jest.mock('../ce');
 
 export default class ComputeEngineServiceMock {
   tasks: Task[];
+  taskWarnings: TaskWarning[] = [];
   workers = { ...DEFAULT_WORKERS };
 
   constructor() {
@@ -75,6 +83,7 @@ export default class ComputeEngineServiceMock {
     jest.mocked(getWorkers).mockImplementation(this.handleGetWorkers);
     jest.mocked(setWorkerCount).mockImplementation(this.handleSetWorkerCount);
     jest.mocked(getTasksForComponent).mockImplementation(this.handleGetTaskForComponent);
+    jest.mocked(getAnalysisStatus).mockImplementation(this.handleAnalysisStatus);
 
     this.tasks = cloneDeep(DEFAULT_TASKS);
   }
@@ -89,6 +98,22 @@ export default class ComputeEngineServiceMock {
     return Promise.resolve();
   };
 
+  setTaskWarnings = (taskWarnings: TaskWarning[] = []) => {
+    this.taskWarnings = taskWarnings;
+  };
+
+  handleAnalysisStatus = (data: { component: string; branch?: string; pullRequest?: string }) => {
+    return Promise.resolve({
+      component: {
+        key: data.component,
+        name: data.component,
+        branch: data.branch,
+        pullRequest: data.pullRequest,
+        warnings: this.taskWarnings,
+      },
+    });
+  };
+
   handleCancelTask = (id: string) => {
     const task = this.tasks.find((t) => t.id === id);
 
@@ -217,6 +242,7 @@ export default class ComputeEngineServiceMock {
 
   reset() {
     this.tasks = cloneDeep(DEFAULT_TASKS);
+    this.taskWarnings = [];
     this.workers = { ...DEFAULT_WORKERS };
   }
 
index 829359c6353eb8db0ca4f7f0de078664b15197a0..b8edf5e2c4d48c055cf9a758d097fc2ac85ff7b9 100644 (file)
@@ -258,12 +258,13 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>
   const componentProviderProps = React.useMemo(
     () => ({
       component,
+      currentTask,
       isInProgress,
       isPending,
       onComponentChange: handleComponentChange,
       fetchComponent,
     }),
-    [component, isInProgress, isPending, handleComponentChange, fetchComponent],
+    [component, currentTask, isInProgress, isPending, handleComponentChange, fetchComponent],
   );
 
   // Show not found component when, after loading:
@@ -289,7 +290,6 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>
         createPortal(
           <ComponentNav
             component={component}
-            currentTask={currentTask}
             isInProgress={isInProgress}
             isPending={isPending}
             projectBindingErrors={projectBindingErrors}
index 410f3034527c3c8cb7f418cde9d10881e812aece..90cc157cdaced071713a7cef5ec0c1b9d607894f 100644 (file)
@@ -39,3 +39,7 @@ export default function withComponentContext<P extends Partial<ComponentContextS
     }
   };
 }
+
+export function useComponent() {
+  return React.useContext(ComponentContext);
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx
deleted file mode 100644 (file)
index d9536cd..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { Link } from 'design-system';
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { useLocation } from 'react-router-dom';
-import { isBranch, isMainBranch, isPullRequest } from '../../../../helpers/branch-like';
-import { hasMessage, translate } from '../../../../helpers/l10n';
-import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls';
-import { useBranchesQuery } from '../../../../queries/branch';
-import { BranchLike } from '../../../../types/branch-like';
-import { Task } from '../../../../types/tasks';
-import { Component } from '../../../../types/types';
-
-interface Props {
-  component: Component;
-  currentTask: Task;
-  onLeave: () => void;
-}
-
-function isSameBranch(task: Task, branchLike?: BranchLike) {
-  if (branchLike) {
-    if (isMainBranch(branchLike)) {
-      return (!task.pullRequest && !task.branch) || branchLike.name === task.branch;
-    }
-    if (isPullRequest(branchLike)) {
-      return branchLike.key === task.pullRequest;
-    }
-    if (isBranch(branchLike)) {
-      return branchLike.name === task.branch;
-    }
-  }
-  return !task.branch && !task.pullRequest;
-}
-
-export function AnalysisErrorMessage(props: Props) {
-  const { component, currentTask } = props;
-  const { data: { branchLike } = {} } = useBranchesQuery(component);
-  const currentTaskOnSameBranch = isSameBranch(currentTask, branchLike);
-
-  const location = useLocation();
-
-  const backgroundTaskUrl = getComponentBackgroundTaskUrl(component.key);
-  const canSeeBackgroundTasks = component.configuration?.showBackgroundTasks;
-  const isOnBackgroundTaskPage = location.pathname === backgroundTaskUrl.pathname;
-
-  const branch =
-    currentTask.branch ??
-    `${currentTask.pullRequest ?? ''}${
-      currentTask.pullRequestTitle ? ' - ' + currentTask.pullRequestTitle : ''
-    }`;
-
-  let messageKey;
-  if (currentTaskOnSameBranch === false && branch) {
-    messageKey = 'component_navigation.status.failed_branch';
-  } else {
-    messageKey = 'component_navigation.status.failed';
-  }
-
-  let type;
-  if (hasMessage('background_task.type', currentTask.type)) {
-    messageKey += '_X';
-    type = translate('background_task.type', currentTask.type);
-  }
-
-  let url;
-  let stacktrace;
-  if (canSeeBackgroundTasks) {
-    messageKey += '.admin';
-
-    if (isOnBackgroundTaskPage) {
-      messageKey += '.help';
-      stacktrace = translate('background_tasks.show_stacktrace');
-    } else {
-      messageKey += '.link';
-      url = (
-        <Link onClick={props.onLeave} to={backgroundTaskUrl}>
-          {translate('background_tasks.page')}
-        </Link>
-      );
-    }
-  }
-
-  return (
-    <FormattedMessage
-      defaultMessage={translate(messageKey)}
-      id={messageKey}
-      values={{ branch, url, stacktrace, type }}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx
deleted file mode 100644 (file)
index 6005838..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 * as React from 'react';
-import Modal from '../../../../components/controls/Modal';
-import { ResetButtonLink } from '../../../../components/controls/buttons';
-import { hasMessage, translate } from '../../../../helpers/l10n';
-import { Task } from '../../../../types/tasks';
-import { Component } from '../../../../types/types';
-import { AnalysisErrorMessage } from './AnalysisErrorMessage';
-import { AnalysisLicenseError } from './AnalysisLicenseError';
-
-interface Props {
-  component: Component;
-  currentTask: Task;
-  onClose: () => void;
-}
-
-export function AnalysisErrorModal(props: Props) {
-  const { component, currentTask } = props;
-
-  const header = translate('error');
-
-  const licenseError =
-    currentTask.errorType &&
-    hasMessage('license.component_navigation.button', currentTask.errorType);
-
-  return (
-    <Modal contentLabel={header} onRequestClose={props.onClose}>
-      <header className="modal-head">
-        <h2>{header}</h2>
-      </header>
-
-      <div className="modal-body modal-container">
-        {licenseError ? (
-          <AnalysisLicenseError currentTask={currentTask} />
-        ) : (
-          <AnalysisErrorMessage
-            component={component}
-            currentTask={currentTask}
-            onLeave={props.onClose}
-          />
-        )}
-      </div>
-
-      <footer className="modal-foot">
-        <ResetButtonLink onClick={props.onClose}>{translate('close')}</ResetButtonLink>
-      </footer>
-    </Modal>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisLicenseError.tsx
deleted file mode 100644 (file)
index 38a5181..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 * as React from 'react';
-import Link from '../../../../components/common/Link';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { ComponentQualifier } from '../../../../types/component';
-import { Task } from '../../../../types/tasks';
-import { AppStateContext } from '../../app-state/AppStateContext';
-import { useLicenseIsValid } from './useLicenseIsValid';
-
-interface Props {
-  currentTask: Task;
-}
-
-export function AnalysisLicenseError(props: Props) {
-  const { currentTask } = props;
-  const appState = React.useContext(AppStateContext);
-  const [licenseIsValid, loading] = useLicenseIsValid();
-
-  if (loading || !currentTask.errorType) {
-    return null;
-  }
-
-  if (licenseIsValid && currentTask.errorType !== 'LICENSING_LOC') {
-    return (
-      <>
-        {translateWithParameters(
-          'component_navigation.status.last_blocked_due_to_bad_license_X',
-          translate('qualifier', currentTask.componentQualifier ?? ComponentQualifier.Project),
-        )}
-      </>
-    );
-  }
-
-  return (
-    <>
-      <span className="little-spacer-right">{currentTask.errorMessage}</span>
-      {appState.canAdmin ? (
-        <Link to="/admin/extension/license/app">
-          {translate('license.component_navigation.button', currentTask.errorType)}.
-        </Link>
-      ) : (
-        translate('please_contact_administrator')
-      )}
-    </>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx
deleted file mode 100644 (file)
index b6408f0..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { FlagMessage, Link, Spinner } from 'design-system';
-import * as React from 'react';
-import { translate } from '../../../../helpers/l10n';
-import { useBranchWarningQuery } from '../../../../queries/branch';
-import { Task, TaskStatuses } from '../../../../types/tasks';
-import { Component } from '../../../../types/types';
-import { AnalysisErrorModal } from './AnalysisErrorModal';
-import AnalysisWarningsModal from './AnalysisWarningsModal';
-
-export interface HeaderMetaProps {
-  currentTask?: Task;
-  component: Component;
-  isInProgress?: boolean;
-  isPending?: boolean;
-}
-
-export function AnalysisStatus(props: HeaderMetaProps) {
-  const { component, currentTask, isInProgress, isPending } = props;
-  const { data: warnings, isLoading } = useBranchWarningQuery(component);
-
-  const [modalIsVisible, setDisplayModal] = React.useState(false);
-  const openModal = React.useCallback(() => {
-    setDisplayModal(true);
-  }, [setDisplayModal]);
-  const closeModal = React.useCallback(() => {
-    setDisplayModal(false);
-  }, [setDisplayModal]);
-
-  if (isInProgress || isPending) {
-    return (
-      <div className="sw-flex sw-items-center">
-        <Spinner />
-        <span className="sw-ml-1">
-          {isInProgress
-            ? translate('project_navigation.analysis_status.in_progress')
-            : translate('project_navigation.analysis_status.pending')}
-        </span>
-      </div>
-    );
-  }
-
-  if (currentTask?.status === TaskStatuses.Failed) {
-    return (
-      <>
-        <FlagMessage variant="error">
-          <span>{translate('project_navigation.analysis_status.failed')}</span>
-          <Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>
-            {translate('project_navigation.analysis_status.details_link')}
-          </Link>
-        </FlagMessage>
-        {modalIsVisible && (
-          <AnalysisErrorModal
-            component={component}
-            currentTask={currentTask}
-            onClose={closeModal}
-          />
-        )}
-      </>
-    );
-  }
-
-  if (!isLoading && warnings && warnings.length > 0) {
-    return (
-      <>
-        <FlagMessage variant="warning">
-          <span>{translate('project_navigation.analysis_status.warnings')}</span>
-          <Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>
-            {translate('project_navigation.analysis_status.details_link')}
-          </Link>
-        </FlagMessage>
-        {modalIsVisible && (
-          <AnalysisWarningsModal component={component} onClose={closeModal} warnings={warnings} />
-        )}
-      </>
-    );
-  }
-
-  return null;
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx
deleted file mode 100644 (file)
index 45914a2..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { DangerButtonSecondary, FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system';
-import * as React from 'react';
-import { translate } from '../../../../helpers/l10n';
-import { sanitizeStringRestricted } from '../../../../helpers/sanitize';
-import { useDismissBranchWarningMutation } from '../../../../queries/branch';
-import { TaskWarning } from '../../../../types/tasks';
-import { Component } from '../../../../types/types';
-import { CurrentUser } from '../../../../types/users';
-import withCurrentUserContext from '../../current-user/withCurrentUserContext';
-
-interface Props {
-  component: Component;
-  currentUser: CurrentUser;
-  onClose: () => void;
-  warnings: TaskWarning[];
-}
-
-export function AnalysisWarningsModal(props: Props) {
-  const { component, currentUser, warnings } = props;
-
-  const { mutate, isLoading, variables } = useDismissBranchWarningMutation();
-
-  const handleDismissMessage = (messageKey: string) => {
-    mutate({ component, key: messageKey });
-  };
-
-  const body = (
-    <>
-      {warnings.map(({ dismissable, key, message }) => (
-        <React.Fragment key={key}>
-          <div className="sw-flex sw-items-center sw-mt-2">
-            <FlagMessage variant="warning">
-              <HtmlFormatter>
-                <span
-                  // eslint-disable-next-line react/no-danger
-                  dangerouslySetInnerHTML={{
-                    __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
-                  }}
-                />
-              </HtmlFormatter>
-            </FlagMessage>
-          </div>
-          <div>
-            {dismissable && currentUser.isLoggedIn && (
-              <div className="sw-mt-4">
-                <DangerButtonSecondary
-                  disabled={Boolean(isLoading)}
-                  onClick={() => {
-                    handleDismissMessage(key);
-                  }}
-                >
-                  {translate('dismiss_permanently')}
-                </DangerButtonSecondary>
-
-                <Spinner className="sw-ml-2" loading={isLoading && variables?.key === key} />
-              </div>
-            )}
-          </div>
-        </React.Fragment>
-      ))}
-    </>
-  );
-
-  return (
-    <Modal
-      headerTitle={translate('warnings')}
-      onClose={props.onClose}
-      body={body}
-      primaryButton={null}
-      secondaryButtonLabel={translate('close')}
-    />
-  );
-}
-
-export default withCurrentUserContext(AnalysisWarningsModal);
index 206b350658ccf382c60e12ac402e5daac0a3e62a..cab37894a900d582b955f2e1e3d52ac4e31b6afe 100644 (file)
@@ -26,7 +26,6 @@ import { ProjectAlmBindingConfigurationErrors } from '../../../../types/alm-sett
 import { Branch } from '../../../../types/branch-like';
 import { ComponentQualifier } from '../../../../types/component';
 import { Feature } from '../../../../types/features';
-import { Task } from '../../../../types/tasks';
 import { Component } from '../../../../types/types';
 import RecentHistory from '../../RecentHistory';
 import withAvailableFeatures, {
@@ -34,28 +33,19 @@ import withAvailableFeatures, {
 } from '../../available-features/withAvailableFeatures';
 import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif';
 import Header from './Header';
-import HeaderMeta from './HeaderMeta';
 import Menu from './Menu';
 
 export interface ComponentNavProps extends WithAvailableFeaturesProps {
   branchLike?: Branch;
   component: Component;
-  currentTask?: Task;
   isInProgress?: boolean;
   isPending?: boolean;
   projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
 }
 
 function ComponentNav(props: Readonly<ComponentNavProps>) {
-  const {
-    branchLike,
-    component,
-    currentTask,
-    hasFeature,
-    isInProgress,
-    isPending,
-    projectBindingErrors,
-  } = props;
+  const { branchLike, component, hasFeature, isInProgress, isPending, projectBindingErrors } =
+    props;
 
   React.useEffect(() => {
     const { breadcrumbs, key, name } = component;
@@ -76,12 +66,6 @@ function ComponentNav(props: Readonly<ComponentNavProps>) {
       <TopBar id="context-navigation" aria-label={translate('qualifier', component.qualifier)}>
         <div className="sw-min-h-10 sw-flex sw-justify-between">
           <Header component={component} />
-          <HeaderMeta
-            component={component}
-            currentTask={currentTask}
-            isInProgress={isInProgress}
-            isPending={isPending}
-          />
         </div>
         <Menu component={component} isInProgress={isInProgress} isPending={isPending} />
       </TopBar>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
deleted file mode 100644 (file)
index b6ed7b4..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { TextMuted } from 'design-system';
-import * as React from 'react';
-import HomePageSelect from '../../../../components/controls/HomePageSelect';
-import { isBranch, isPullRequest } from '../../../../helpers/branch-like';
-import { translateWithParameters } from '../../../../helpers/l10n';
-import { useBranchesQuery } from '../../../../queries/branch';
-import { Task } from '../../../../types/tasks';
-import { Component } from '../../../../types/types';
-import { CurrentUser, isLoggedIn } from '../../../../types/users';
-import withCurrentUserContext from '../../current-user/withCurrentUserContext';
-import { AnalysisStatus } from './AnalysisStatus';
-import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMergeInformation';
-import { getCurrentPage } from './utils';
-
-export interface HeaderMetaProps {
-  component: Component;
-  currentUser: CurrentUser;
-  currentTask?: Task;
-  isInProgress?: boolean;
-  isPending?: boolean;
-}
-
-export function HeaderMeta(props: HeaderMetaProps) {
-  const { component, currentUser, currentTask, isInProgress, isPending } = props;
-
-  const { data: { branchLike } = {} } = useBranchesQuery(component);
-
-  const isABranch = isBranch(branchLike);
-
-  const currentPage = getCurrentPage(component, branchLike);
-
-  return (
-    <div className="sw-flex sw-items-center sw-flex-shrink sw-min-w-0">
-      <AnalysisStatus
-        component={component}
-        currentTask={currentTask}
-        isInProgress={isInProgress}
-        isPending={isPending}
-      />
-      {branchLike && <CurrentBranchLikeMergeInformation currentBranchLike={branchLike} />}
-      {component.version !== undefined && isABranch && (
-        <TextMuted
-          text={translateWithParameters('version_x', component.version)}
-          className="sw-ml-4 sw-whitespace-nowrap"
-        />
-      )}
-      {isLoggedIn(currentUser) && currentPage !== undefined && !isPullRequest(branchLike) && (
-        <HomePageSelect className="sw-ml-2" currentPage={currentPage} />
-      )}
-    </div>
-  );
-}
-
-export default withCurrentUserContext(HeaderMeta);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx
deleted file mode 100644 (file)
index b30d3bc..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
-import * as React from 'react';
-import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
-import { mockComponent } from '../../../../../helpers/mocks/component';
-import { mockTask } from '../../../../../helpers/mocks/tasks';
-import { renderApp } from '../../../../../helpers/testReactTestingUtils';
-import { Feature } from '../../../../../types/features';
-import { AnalysisErrorMessage } from '../AnalysisErrorMessage';
-
-const handler = new BranchesServiceMock();
-
-beforeEach(() => {
-  handler.reset();
-});
-
-it('should work when error is on a different branch', () => {
-  renderAnalysisErrorMessage({
-    currentTask: mockTask({ branch: 'branch-1.2' }),
-  });
-
-  expect(screen.getByText(/component_navigation.status.failed_branch_X/)).toBeInTheDocument();
-  expect(screen.getByText(/branch-1\.2/)).toBeInTheDocument();
-});
-
-it('should work for errors on Pull Requests', async () => {
-  renderAnalysisErrorMessage(
-    {
-      currentTask: mockTask({ pullRequest: '01', pullRequestTitle: 'Fix stuff' }),
-    },
-    undefined,
-    'pullRequest=01&id=my-project',
-  );
-
-  expect(await screen.findByText(/component_navigation.status.failed_X/)).toBeInTheDocument();
-  expect(screen.getByText(/01 - Fix stuff/)).toBeInTheDocument();
-});
-
-it('should provide a link to admins', () => {
-  renderAnalysisErrorMessage({
-    component: mockComponent({ configuration: { showBackgroundTasks: true } }),
-  });
-
-  expect(screen.getByText(/component_navigation.status.failed_X.admin.link/)).toBeInTheDocument();
-  expect(screen.getByRole('link', { name: 'background_tasks.page' })).toBeInTheDocument();
-});
-
-it('should explain to admins how to get the staktrace', () => {
-  renderAnalysisErrorMessage(
-    {
-      component: mockComponent({ configuration: { showBackgroundTasks: true } }),
-    },
-    'project/background_tasks',
-  );
-
-  expect(screen.getByText(/component_navigation.status.failed_X.admin.help/)).toBeInTheDocument();
-  expect(screen.queryByRole('link', { name: 'background_tasks.page' })).not.toBeInTheDocument();
-});
-
-function renderAnalysisErrorMessage(
-  overrides: Partial<Parameters<typeof AnalysisErrorMessage>[0]> = {},
-  location = '/',
-  params?: string,
-) {
-  return renderApp(
-    location,
-    <AnalysisErrorMessage
-      component={mockComponent()}
-      currentTask={mockTask()}
-      onLeave={jest.fn()}
-      {...overrides}
-    />,
-    { navigateTo: params ? `/?${params}` : undefined, featureList: [Feature.BranchSupport] },
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisLicenseError-test.tsx
deleted file mode 100644 (file)
index 43ffde1..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
-import * as React from 'react';
-import { isValidLicense } from '../../../../../api/editions';
-import { mockTask } from '../../../../../helpers/mocks/tasks';
-import { mockAppState } from '../../../../../helpers/testMocks';
-import { renderApp } from '../../../../../helpers/testReactTestingUtils';
-import { AnalysisLicenseError } from '../AnalysisLicenseError';
-
-jest.mock('../../../../../api/editions', () => ({
-  isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: true }),
-}));
-
-it('should handle a valid license', async () => {
-  renderAnalysisLicenseError({
-    currentTask: mockTask({ errorType: 'ANY_TYPE' }),
-  });
-
-  expect(
-    await screen.findByText(
-      'component_navigation.status.last_blocked_due_to_bad_license_X.qualifier.TRK',
-    ),
-  ).toBeInTheDocument();
-});
-
-it('should send user to contact the admin', async () => {
-  const errorMessage = 'error message';
-  renderAnalysisLicenseError({
-    currentTask: mockTask({ errorMessage, errorType: 'LICENSING_LOC' }),
-  });
-
-  expect(await screen.findByText('please_contact_administrator')).toBeInTheDocument();
-  expect(screen.getByText(errorMessage)).toBeInTheDocument();
-});
-
-it('should send provide a link to the admin', async () => {
-  jest.mocked(isValidLicense).mockResolvedValueOnce({ isValidLicense: false });
-
-  const errorMessage = 'error message';
-  renderAnalysisLicenseError(
-    {
-      currentTask: mockTask({ errorMessage, errorType: 'error-type' }),
-    },
-    true,
-  );
-
-  expect(
-    await screen.findByText('license.component_navigation.button.error-type.'),
-  ).toBeInTheDocument();
-  expect(screen.getByText(errorMessage)).toBeInTheDocument();
-});
-
-function renderAnalysisLicenseError(
-  overrides: Partial<Parameters<typeof AnalysisLicenseError>[0]> = {},
-  canAdmin = false,
-) {
-  return renderApp('/', <AnalysisLicenseError currentTask={mockTask()} {...overrides} />, {
-    appState: mockAppState({ canAdmin }),
-  });
-}
index 9ed116e208ee5b5c2b3826990aab4fc521bdf55b..c711da7c9382023c1dc353c503fc781273c3e6b3 100644 (file)
@@ -21,33 +21,10 @@ import { screen } from '@testing-library/react';
 import React from 'react';
 import { mockProjectAlmBindingConfigurationErrors } from '../../../../../helpers/mocks/alm-settings';
 import { mockComponent } from '../../../../../helpers/mocks/component';
-import { mockTask } from '../../../../../helpers/mocks/tasks';
 import { renderApp } from '../../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../../types/component';
-import { TaskStatuses } from '../../../../../types/tasks';
 import ComponentNav, { ComponentNavProps } from '../ComponentNav';
 
-it('renders correctly when there is a background task in progress', () => {
-  renderComponentNav({ isInProgress: true });
-  expect(
-    screen.getByText('project_navigation.analysis_status.in_progress', { exact: false }),
-  ).toBeInTheDocument();
-});
-
-it('renders correctly when there is a background task pending', () => {
-  renderComponentNav({ isPending: true });
-  expect(
-    screen.getByText('project_navigation.analysis_status.pending', { exact: false }),
-  ).toBeInTheDocument();
-});
-
-it('renders correctly when there is a failing background task', () => {
-  renderComponentNav({ currentTask: mockTask({ status: TaskStatuses.Failed }) });
-  expect(
-    screen.getByText('project_navigation.analysis_status.failed', { exact: false }),
-  ).toBeInTheDocument();
-});
-
 it('renders correctly when the project binding is incorrect', () => {
   renderComponentNav({
     projectBindingErrors: mockProjectAlmBindingConfigurationErrors(),
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
deleted file mode 100644 (file)
index 5685ac0..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import { getAnalysisStatus } from '../../../../../api/ce';
-import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
-import { mockComponent } from '../../../../../helpers/mocks/component';
-import { mockTask } from '../../../../../helpers/mocks/tasks';
-import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks';
-import { renderApp } from '../../../../../helpers/testReactTestingUtils';
-import { Feature } from '../../../../../types/features';
-import { TaskStatuses } from '../../../../../types/tasks';
-import { CurrentUser } from '../../../../../types/users';
-import HeaderMeta, { HeaderMetaProps } from '../HeaderMeta';
-
-jest.mock('../../../../../api/ce');
-
-const handler = new BranchesServiceMock();
-
-beforeEach(() => handler.reset());
-
-it('should render correctly for a branch with warnings', async () => {
-  const user = userEvent.setup();
-  jest.mocked(getAnalysisStatus).mockResolvedValue({
-    component: {
-      warnings: [{ dismissable: false, key: 'key', message: 'bar' }],
-      key: 'compkey',
-      name: 'me',
-    },
-  });
-  renderHeaderMeta({}, undefined, 'branch=normal-branch&id=my-project');
-
-  expect(await screen.findByText('version_x.0.0.1')).toBeInTheDocument();
-
-  expect(
-    await screen.findByText('project_navigation.analysis_status.warnings'),
-  ).toBeInTheDocument();
-
-  await user.click(screen.getByText('project_navigation.analysis_status.details_link'));
-
-  expect(screen.getByRole('heading', { name: 'warnings' })).toBeInTheDocument();
-});
-
-it('should handle a branch with missing version and no warnings', () => {
-  jest.mocked(getAnalysisStatus).mockResolvedValue({
-    component: {
-      warnings: [],
-      key: 'compkey',
-      name: 'me',
-    },
-  });
-  renderHeaderMeta({ component: mockComponent({ version: undefined }) });
-
-  expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument();
-  expect(screen.queryByText('project_navigation.analysis_status.warnings')).not.toBeInTheDocument();
-});
-
-it('should render correctly with a failed analysis', async () => {
-  const user = userEvent.setup();
-
-  renderHeaderMeta({
-    currentTask: mockTask({
-      status: TaskStatuses.Failed,
-      errorMessage: 'this is the error message',
-    }),
-  });
-
-  expect(await screen.findByText('project_navigation.analysis_status.failed')).toBeInTheDocument();
-
-  await user.click(screen.getByText('project_navigation.analysis_status.details_link'));
-
-  expect(screen.getByRole('heading', { name: 'error' })).toBeInTheDocument();
-});
-
-it('should render correctly for a pull request', async () => {
-  renderHeaderMeta({}, undefined, 'pullRequest=01&id=my-project');
-
-  expect(
-    await screen.findByText('branch_like_navigation.for_merge_into_x_from_y'),
-  ).toBeInTheDocument();
-  expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument();
-});
-
-it('should render correctly when the user is not logged in', () => {
-  renderHeaderMeta({}, mockCurrentUser({ dismissedNotices: {} }));
-  expect(screen.queryByText('homepage.current.is_default')).not.toBeInTheDocument();
-  expect(screen.queryByText('homepage.current')).not.toBeInTheDocument();
-  expect(screen.queryByText('homepage.check')).not.toBeInTheDocument();
-});
-
-function renderHeaderMeta(
-  props: Partial<HeaderMetaProps> = {},
-  currentUser: CurrentUser = mockLoggedInUser(),
-  params?: string,
-) {
-  return renderApp('/', <HeaderMeta component={mockComponent({ version: '0.0.1' })} {...props} />, {
-    currentUser,
-    navigateTo: params ? `/?id=my-project&${params}` : '/?id=my-project',
-    featureList: [Feature.BranchSupport],
-  });
-}
index bdb7bd1e267e2c4e4bfa08439d2602ba8fb5fa64..d8d7c24da37dbb8548a15f694f8f59c2182badc4 100644 (file)
  */
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { isPullRequest } from '../../../../../helpers/branch-like';
 import { translate, translateWithParameters } from '../../../../../helpers/l10n';
-import { BranchLike } from '../../../../../types/branch-like';
+import { PullRequest } from '../../../../../types/branch-like';
 
 export interface CurrentBranchLikeMergeInformationProps {
-  currentBranchLike: BranchLike;
+  pullRequest: PullRequest;
 }
 
-export function CurrentBranchLikeMergeInformation(props: CurrentBranchLikeMergeInformationProps) {
-  const { currentBranchLike } = props;
-
-  if (!isPullRequest(currentBranchLike)) {
-    return null;
-  }
-
+export function CurrentBranchLikeMergeInformation({
+  pullRequest,
+}: Readonly<CurrentBranchLikeMergeInformationProps>) {
   return (
     <span
-      className="sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-mx-1 sw-flex-shrink sw-min-w-0"
+      className="sw-w-[400px] sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden sw-flex-shrink sw-min-w-0"
       title={translateWithParameters(
         'branch_like_navigation.for_merge_into_x_from_y.title',
-        currentBranchLike.target,
-        currentBranchLike.branch,
+        pullRequest.target,
+        pullRequest.branch,
       )}
     >
       <FormattedMessage
         defaultMessage={translate('branch_like_navigation.for_merge_into_x_from_y')}
         id="branch_like_navigation.for_merge_into_x_from_y"
         values={{
-          target: <strong>{currentBranchLike.target}</strong>,
-          branch: <strong>{currentBranchLike.branch}</strong>,
+          target: <strong>{pullRequest.target}</strong>,
+          branch: <strong>{pullRequest.branch}</strong>,
         }}
       />
     </span>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts b/server/sonar-web/src/main/js/app/components/nav/component/useLicenseIsValid.ts
deleted file mode 100644 (file)
index 979e009..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 React, { useEffect } from 'react';
-import { isValidLicense } from '../../../../api/editions';
-
-export function useLicenseIsValid(): [boolean, boolean] {
-  const [licenseIsValid, setLicenseIsValid] = React.useState(false);
-  const [loading, setLoading] = React.useState(true);
-
-  useEffect(() => {
-    setLoading(true);
-
-    isValidLicense()
-      .then(({ isValidLicense }) => {
-        setLicenseIsValid(isValidLicense);
-        setLoading(false);
-      })
-      .catch(() => {
-        setLoading(false);
-      });
-  }, []);
-
-  return [licenseIsValid, loading];
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx
new file mode 100644 (file)
index 0000000..d40efb4
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { SeparatorCircleIcon } from 'design-system';
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { getCurrentPage } from '../../../app/components/nav/component/utils';
+import ComponentReportActions from '../../../components/controls/ComponentReportActions';
+import HomePageSelect from '../../../components/controls/HomePageSelect';
+import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { Branch } from '../../../types/branch-like';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import { HomePage } from '../../../types/users';
+
+interface Props {
+  component: Component;
+  branch: Branch;
+  measures: MeasureEnhanced[];
+}
+
+export default function BranchMetaTopBar({ branch, measures, component }: Readonly<Props>) {
+  const intl = useIntl();
+
+  const currentPage = getCurrentPage(component, branch) as HomePage;
+  const locMeasure = findMeasure(measures, MetricKey.lines);
+
+  const leftSection = (
+    <h1 className="sw-flex sw-gap-2 sw-items-center sw-heading-md">{branch.name}</h1>
+  );
+  const rightSection = (
+    <div className="sw-flex sw-gap-2 sw-items-center">
+      {locMeasure && (
+        <>
+          <div className="sw-flex sw-items-center sw-gap-1">
+            <strong>{formatMeasure(locMeasure.value, MetricType.ShortInteger)}</strong>
+            {intl.formatMessage({ id: 'metric.ncloc.name' })}
+          </div>
+          <SeparatorCircleIcon />
+        </>
+      )}
+      {component.version && (
+        <>
+          <div className="sw-flex sw-items-center sw-gap-1">
+            {intl.formatMessage({ id: 'version_x' }, { '0': <strong>{component.version}</strong> })}
+          </div>
+          <SeparatorCircleIcon />
+        </>
+      )}
+      <HomePageSelect currentPage={currentPage} type="button" />
+      <ComponentReportActions component={component} branch={branch} />
+    </div>
+  );
+
+  return (
+    <div className="sw-flex sw-justify-between sw-whitespace-nowrap sw-body-sm sw-mb-2">
+      {leftSection}
+      {rightSection}
+    </div>
+  );
+}
index 4bccbf849fc7543015301c170c9e1de0bc7f81c6..0dca8d3241eafc2946014cd544b1755c849e121b 100644 (file)
@@ -17,7 +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 { LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
+import { BasicSeparator, LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
 import * as React from 'react';
 import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
 import { useLocation } from '../../../components/hoc/withRouter';
@@ -30,9 +30,11 @@ import { ComponentQualifier } from '../../../types/component';
 import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
 import { QualityGateStatus } from '../../../types/quality-gates';
 import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
+import { AnalysisStatus } from '../components/AnalysisStatus';
 import { MeasuresTabs } from '../utils';
 import AcceptedIssuesPanel from './AcceptedIssuesPanel';
 import ActivityPanel from './ActivityPanel';
+import BranchMetaTopBar from './BranchMetaTopBar';
 import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
 import MeasuresPanel from './MeasuresPanel';
 import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
@@ -115,66 +117,74 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
             {projectIsEmpty ? (
               <NoCodeWarning branchLike={branch} component={component} measures={measures} />
             ) : (
-              <div className="sw-flex">
-                <div className="sw-w-1/3 sw-mr-12 sw-pt-6">
-                  <QualityGatePanel
-                    component={component}
-                    loading={loadingStatus}
-                    qgStatuses={qgStatuses}
-                    qualityGate={qualityGate}
-                  />
-                </div>
-
-                <div className="sw-flex-1">
-                  <div className="sw-flex sw-flex-col sw-pt-6">
-                    <TabsPanel
-                      analyses={analyses}
-                      appLeak={appLeak}
-                      branch={branch}
+              <div>
+                {branch && (
+                  <>
+                    <BranchMetaTopBar branch={branch} component={component} measures={measures} />
+                    <BasicSeparator />
+                  </>
+                )}
+                <AnalysisStatus className="sw-mt-6" component={component} />
+                <div className="sw-flex">
+                  <div className="sw-w-1/3 sw-mr-12 sw-pt-6">
+                    <QualityGatePanel
                       component={component}
                       loading={loadingStatus}
-                      period={period}
                       qgStatuses={qgStatuses}
-                      isNewCode={isNewCodeTab}
-                      onTabSelect={selectTab}
-                    >
-                      {!hasNewCodeMeasures && isNewCodeTab ? (
-                        <MeasuresPanelNoNewCode
-                          branch={branch}
-                          component={component}
-                          period={period}
-                        />
-                      ) : (
-                        <>
-                          <MeasuresPanel
-                            branch={branch}
-                            component={component}
-                            measures={measures}
-                            isNewCode={isNewCodeTab}
-                          />
+                      qualityGate={qualityGate}
+                    />
+                  </div>
 
-                          <AcceptedIssuesPanel
+                  <div className="sw-flex-1">
+                    <div className="sw-flex sw-flex-col sw-pt-6">
+                      <TabsPanel
+                        analyses={analyses}
+                        appLeak={appLeak}
+                        component={component}
+                        loading={loadingStatus}
+                        period={period}
+                        qgStatuses={qgStatuses}
+                        isNewCode={isNewCodeTab}
+                        onTabSelect={selectTab}
+                      >
+                        {!hasNewCodeMeasures && isNewCodeTab ? (
+                          <MeasuresPanelNoNewCode
                             branch={branch}
                             component={component}
-                            measures={measures}
-                            isNewCode={isNewCodeTab}
-                            loading={loadingStatus}
+                            period={period}
                           />
-                        </>
-                      )}
-                    </TabsPanel>
+                        ) : (
+                          <>
+                            <MeasuresPanel
+                              branch={branch}
+                              component={component}
+                              measures={measures}
+                              isNewCode={isNewCodeTab}
+                            />
 
-                    <ActivityPanel
-                      analyses={analyses}
-                      branchLike={branch}
-                      component={component}
-                      graph={graph}
-                      leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)}
-                      loading={loadingHistory}
-                      measuresHistory={measuresHistory}
-                      metrics={metrics}
-                      onGraphChange={onGraphChange}
-                    />
+                            <AcceptedIssuesPanel
+                              branch={branch}
+                              component={component}
+                              measures={measures}
+                              isNewCode={isNewCodeTab}
+                              loading={loadingStatus}
+                            />
+                          </>
+                        )}
+                      </TabsPanel>
+
+                      <ActivityPanel
+                        analyses={analyses}
+                        branchLike={branch}
+                        component={component}
+                        graph={graph}
+                        leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)}
+                        loading={loadingHistory}
+                        measuresHistory={measuresHistory}
+                        metrics={metrics}
+                        onGraphChange={onGraphChange}
+                      />
+                    </div>
                   </div>
                 </div>
               </div>
index 258dffa8b6e7d20e4ab86819e7e4c33dfcfc8e65..9756c817ae9d60da6ba19c103a58a5fcf0b2d8d5 100644 (file)
@@ -29,11 +29,9 @@ import {
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import DocLink from '../../../components/common/DocLink';
-import ComponentReportActions from '../../../components/controls/ComponentReportActions';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { isDiffMetric } from '../../../helpers/measures';
 import { ApplicationPeriod } from '../../../types/application';
-import { Branch } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
 import { Analysis, ProjectAnalysisEventCategory } from '../../../types/project-activity';
 import { QualityGateStatus } from '../../../types/quality-gates';
@@ -45,7 +43,6 @@ import { LeakPeriodInfo } from './LeakPeriodInfo';
 export interface MeasuresPanelProps {
   analyses?: Analysis[];
   appLeak?: ApplicationPeriod;
-  branch?: Branch;
   component: Component;
   loading?: boolean;
   period?: Period;
@@ -60,7 +57,6 @@ export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) {
   const {
     analyses,
     appLeak,
-    branch,
     component,
     loading,
     period,
@@ -128,9 +124,6 @@ export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) {
 
   return (
     <div data-test="overview__measures-panel">
-      <div className="sw-float-right -sw-mt-6">
-        <ComponentReportActions component={component} branch={branch} />
-      </div>
       <div className="sw-flex sw-mb-4">
         <PageTitle as="h2" text={translate('overview.measures')} />
       </div>
index b074b53a63b54db754cdbb9081d04d7d7da4548f..b06c4671c1e985ef7ac7780268fa2ec2843d1aee 100644 (file)
@@ -210,8 +210,12 @@ describe('project overview', () => {
     );
     renderBranchOverview();
 
+    // Meta info
+    expect(await screen.findByText('master')).toBeInTheDocument();
+    expect(screen.getByText('version-1.0')).toBeInTheDocument();
+
     // QG panel
-    expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
+    expect(screen.getByText('metric.level.OK')).toBeInTheDocument();
     expect(screen.getByText('overview.passed.clean_code')).toBeInTheDocument();
     expect(
       screen.queryByText('overview.quality_gate.conditions.cayc.warning'),
@@ -540,6 +544,7 @@ function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) {
           breadcrumbs: [mockComponent({ key: 'foo' })],
           key: 'foo',
           name: 'Foo',
+          version: 'version-1.0',
         })}
         {...props}
       />
diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorMessage.tsx
new file mode 100644 (file)
index 0000000..8a42b1d
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { Link } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { useLocation } from 'react-router-dom';
+import { isBranch, isMainBranch, isPullRequest } from '../../../helpers/branch-like';
+import { hasMessage, translate } from '../../../helpers/l10n';
+import { getComponentBackgroundTaskUrl } from '../../../helpers/urls';
+import { useBranchesQuery } from '../../../queries/branch';
+import { BranchLike } from '../../../types/branch-like';
+import { Task } from '../../../types/tasks';
+import { Component } from '../../../types/types';
+
+interface Props {
+  component: Component;
+  currentTask: Task;
+  onLeave: () => void;
+}
+
+function isSameBranch(task: Task, branchLike?: BranchLike) {
+  if (branchLike) {
+    if (isMainBranch(branchLike)) {
+      return (!task.pullRequest && !task.branch) || branchLike.name === task.branch;
+    }
+    if (isPullRequest(branchLike)) {
+      return branchLike.key === task.pullRequest;
+    }
+    if (isBranch(branchLike)) {
+      return branchLike.name === task.branch;
+    }
+  }
+  return !task.branch && !task.pullRequest;
+}
+
+export function AnalysisErrorMessage(props: Props) {
+  const { component, currentTask } = props;
+  const { data: { branchLike } = {} } = useBranchesQuery(component);
+  const currentTaskOnSameBranch = isSameBranch(currentTask, branchLike);
+
+  const location = useLocation();
+
+  const backgroundTaskUrl = getComponentBackgroundTaskUrl(component.key);
+  const canSeeBackgroundTasks = component.configuration?.showBackgroundTasks;
+  const isOnBackgroundTaskPage = location.pathname === backgroundTaskUrl.pathname;
+
+  const branch =
+    currentTask.branch ??
+    `${currentTask.pullRequest ?? ''}${
+      currentTask.pullRequestTitle ? ' - ' + currentTask.pullRequestTitle : ''
+    }`;
+
+  let messageKey;
+  if (currentTaskOnSameBranch === false && branch) {
+    messageKey = 'component_navigation.status.failed_branch';
+  } else {
+    messageKey = 'component_navigation.status.failed';
+  }
+
+  let type;
+  if (hasMessage('background_task.type', currentTask.type)) {
+    messageKey += '_X';
+    type = translate('background_task.type', currentTask.type);
+  }
+
+  let url;
+  let stacktrace;
+  if (canSeeBackgroundTasks) {
+    messageKey += '.admin';
+
+    if (isOnBackgroundTaskPage) {
+      messageKey += '.help';
+      stacktrace = translate('background_tasks.show_stacktrace');
+    } else {
+      messageKey += '.link';
+      url = (
+        <Link onClick={props.onLeave} to={backgroundTaskUrl}>
+          {translate('background_tasks.page')}
+        </Link>
+      );
+    }
+  }
+
+  return (
+    <FormattedMessage
+      defaultMessage={translate(messageKey)}
+      id={messageKey}
+      values={{ branch, url, stacktrace, type }}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisErrorModal.tsx
new file mode 100644 (file)
index 0000000..3158d29
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 * as React from 'react';
+import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink } from '../../../components/controls/buttons';
+import { hasMessage, translate } from '../../../helpers/l10n';
+import { Task } from '../../../types/tasks';
+import { Component } from '../../../types/types';
+import { AnalysisErrorMessage } from './AnalysisErrorMessage';
+import { AnalysisLicenseError } from './AnalysisLicenseError';
+
+interface Props {
+  component: Component;
+  currentTask: Task;
+  onClose: () => void;
+}
+
+export function AnalysisErrorModal(props: Props) {
+  const { component, currentTask } = props;
+
+  const header = translate('error');
+
+  const licenseError =
+    currentTask.errorType &&
+    hasMessage('license.component_navigation.button', currentTask.errorType);
+
+  return (
+    <Modal contentLabel={header} onRequestClose={props.onClose}>
+      <header className="modal-head">
+        <h2>{header}</h2>
+      </header>
+
+      <div className="modal-body modal-container">
+        {licenseError ? (
+          <AnalysisLicenseError currentTask={currentTask} />
+        ) : (
+          <AnalysisErrorMessage
+            component={component}
+            currentTask={currentTask}
+            onLeave={props.onClose}
+          />
+        )}
+      </div>
+
+      <footer className="modal-foot">
+        <ResetButtonLink onClick={props.onClose}>{translate('close')}</ResetButtonLink>
+      </footer>
+    </Modal>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisLicenseError.tsx
new file mode 100644 (file)
index 0000000..a8f370a
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 * as React from 'react';
+import { AppStateContext } from '../../../app/components/app-state/AppStateContext';
+import Link from '../../../components/common/Link';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { ComponentQualifier } from '../../../types/component';
+import { Task } from '../../../types/tasks';
+import { useLicenseIsValid } from './useLicenseIsValid';
+
+interface Props {
+  currentTask: Task;
+}
+
+export function AnalysisLicenseError(props: Props) {
+  const { currentTask } = props;
+  const appState = React.useContext(AppStateContext);
+  const [licenseIsValid, loading] = useLicenseIsValid();
+
+  if (loading || !currentTask.errorType) {
+    return null;
+  }
+
+  if (licenseIsValid && currentTask.errorType !== 'LICENSING_LOC') {
+    return (
+      <>
+        {translateWithParameters(
+          'component_navigation.status.last_blocked_due_to_bad_license_X',
+          translate('qualifier', currentTask.componentQualifier ?? ComponentQualifier.Project),
+        )}
+      </>
+    );
+  }
+
+  return (
+    <>
+      <span className="little-spacer-right">{currentTask.errorMessage}</span>
+      {appState.canAdmin ? (
+        <Link to="/admin/extension/license/app">
+          {translate('license.component_navigation.button', currentTask.errorType)}.
+        </Link>
+      ) : (
+        translate('please_contact_administrator')
+      )}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisStatus.tsx
new file mode 100644 (file)
index 0000000..df14b94
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 classNames from 'classnames';
+import { FlagMessage, Link, Spinner } from 'design-system';
+import * as React from 'react';
+import { useComponent } from '../../../app/components/componentContext/withComponentContext';
+import { translate } from '../../../helpers/l10n';
+import { useBranchWarningQuery } from '../../../queries/branch';
+import { TaskStatuses } from '../../../types/tasks';
+import { Component } from '../../../types/types';
+import { AnalysisErrorModal } from './AnalysisErrorModal';
+import AnalysisWarningsModal from './AnalysisWarningsModal';
+
+export interface HeaderMetaProps {
+  component: Component;
+  className?: string;
+}
+
+export function AnalysisStatus(props: HeaderMetaProps) {
+  const { className, component } = props;
+  const { currentTask, isPending, isInProgress } = useComponent();
+  const { data: warnings, isLoading } = useBranchWarningQuery(component);
+
+  const [modalIsVisible, setDisplayModal] = React.useState(false);
+  const openModal = React.useCallback(() => {
+    setDisplayModal(true);
+  }, [setDisplayModal]);
+  const closeModal = React.useCallback(() => {
+    setDisplayModal(false);
+  }, [setDisplayModal]);
+
+  if (isInProgress || isPending) {
+    return (
+      <div data-test="analysis-status" className={classNames('sw-flex sw-items-center', className)}>
+        <Spinner />
+        <span className="sw-ml-1">
+          {isInProgress
+            ? translate('project_navigation.analysis_status.in_progress')
+            : translate('project_navigation.analysis_status.pending')}
+        </span>
+      </div>
+    );
+  }
+
+  if (currentTask?.status === TaskStatuses.Failed) {
+    return (
+      <>
+        <FlagMessage data-test="analysis-status" variant="error" className={className}>
+          <span>{translate('project_navigation.analysis_status.failed')}</span>
+          <Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>
+            {translate('project_navigation.analysis_status.details_link')}
+          </Link>
+        </FlagMessage>
+        {modalIsVisible && (
+          <AnalysisErrorModal
+            component={component}
+            currentTask={currentTask}
+            onClose={closeModal}
+          />
+        )}
+      </>
+    );
+  }
+
+  if (!isLoading && warnings && warnings.length > 0) {
+    return (
+      <>
+        <FlagMessage data-test="analysis-status" variant="warning" className={className}>
+          <span>{translate('project_navigation.analysis_status.warnings')}</span>
+          <Link className="sw-ml-1" blurAfterClick onClick={openModal} preventDefault to={{}}>
+            {translate('project_navigation.analysis_status.details_link')}
+          </Link>
+        </FlagMessage>
+        {modalIsVisible && (
+          <AnalysisWarningsModal component={component} onClose={closeModal} warnings={warnings} />
+        )}
+      </>
+    );
+  }
+
+  return null;
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx
new file mode 100644 (file)
index 0000000..d17e2f7
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { DangerButtonSecondary, FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system';
+import * as React from 'react';
+import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
+import { translate } from '../../../helpers/l10n';
+import { sanitizeStringRestricted } from '../../../helpers/sanitize';
+import { useDismissBranchWarningMutation } from '../../../queries/branch';
+import { TaskWarning } from '../../../types/tasks';
+import { Component } from '../../../types/types';
+import { CurrentUser } from '../../../types/users';
+
+interface Props {
+  component: Component;
+  currentUser: CurrentUser;
+  onClose: () => void;
+  warnings: TaskWarning[];
+}
+
+export function AnalysisWarningsModal(props: Props) {
+  const { component, currentUser, warnings } = props;
+
+  const { mutate, isLoading, variables } = useDismissBranchWarningMutation();
+
+  const handleDismissMessage = (messageKey: string) => {
+    mutate({ component, key: messageKey });
+  };
+
+  const body = (
+    <>
+      {warnings.map(({ dismissable, key, message }) => (
+        <React.Fragment key={key}>
+          <div className="sw-flex sw-items-center sw-mt-2">
+            <FlagMessage variant="warning">
+              <HtmlFormatter>
+                <span
+                  // eslint-disable-next-line react/no-danger
+                  dangerouslySetInnerHTML={{
+                    __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
+                  }}
+                />
+              </HtmlFormatter>
+            </FlagMessage>
+          </div>
+          <div>
+            {dismissable && currentUser.isLoggedIn && (
+              <div className="sw-mt-4">
+                <DangerButtonSecondary
+                  disabled={Boolean(isLoading)}
+                  onClick={() => {
+                    handleDismissMessage(key);
+                  }}
+                >
+                  {translate('dismiss_permanently')}
+                </DangerButtonSecondary>
+
+                <Spinner className="sw-ml-2" loading={isLoading && variables?.key === key} />
+              </div>
+            )}
+          </div>
+        </React.Fragment>
+      ))}
+    </>
+  );
+
+  return (
+    <Modal
+      headerTitle={translate('warnings')}
+      onClose={props.onClose}
+      body={body}
+      primaryButton={null}
+      secondaryButtonLabel={translate('close')}
+    />
+  );
+}
+
+export default withCurrentUserContext(AnalysisWarningsModal);
diff --git a/server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx b/server/sonar-web/src/main/js/apps/overview/components/MeasurementLabel.tsx
deleted file mode 100644 (file)
index 8b68d38..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { getLeakValue } from '../../../components/measure/utils';
-import DrilldownLink from '../../../components/shared/DrilldownLink';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
-import { BranchLike } from '../../../types/branch-like';
-import { Component, MeasureEnhanced } from '../../../types/types';
-import {
-  getMeasurementIconClass,
-  getMeasurementLabelKeys,
-  getMeasurementLinesMetricKey,
-  getMeasurementMetricKey,
-  MeasurementType,
-} from '../utils';
-
-interface Props {
-  branchLike?: BranchLike;
-  centered?: boolean;
-  component: Component;
-  measures: MeasureEnhanced[];
-  type: MeasurementType;
-  useDiffMetric?: boolean;
-}
-
-export default class MeasurementLabel extends React.Component<Props> {
-  getLabelText = () => {
-    const { branchLike, component, measures, type, useDiffMetric = false } = this.props;
-    const { expandedLabelKey, labelKey } = getMeasurementLabelKeys(type, useDiffMetric);
-    const linesMetric = getMeasurementLinesMetricKey(type, useDiffMetric);
-    const measure = findMeasure(measures, linesMetric);
-
-    if (!measure) {
-      return translate(labelKey);
-    }
-
-    const value = useDiffMetric ? getLeakValue(measure) : measure.value;
-
-    return (
-      <FormattedMessage
-        defaultMessage={translate(expandedLabelKey)}
-        id={expandedLabelKey}
-        values={{
-          count: (
-            <DrilldownLink
-              branchLike={branchLike}
-              className="big"
-              component={component.key}
-              metric={linesMetric}
-            >
-              {formatMeasure(value, 'SHORT_INT')}
-            </DrilldownLink>
-          ),
-        }}
-      />
-    );
-  };
-
-  render() {
-    const { branchLike, centered, component, measures, type, useDiffMetric = false } = this.props;
-    const iconClass = getMeasurementIconClass(type);
-    const metricKey = getMeasurementMetricKey(type, useDiffMetric);
-    const measure = findMeasure(measures, metricKey);
-
-    let value;
-    if (measure) {
-      value = useDiffMetric ? getLeakValue(measure) : measure.value;
-    }
-
-    if (value === undefined) {
-      return (
-        <div className="display-flex-center">
-          <span aria-label={translate('no_data')} className="overview-measures-empty-value" />
-          <span className="big-spacer-left">{this.getLabelText()}</span>
-        </div>
-      );
-    }
-
-    const icon = React.createElement(iconClass, { size: 'big', value: Number(value) });
-    const formattedValue = formatMeasure(value, 'PERCENT', {
-      decimals: 2,
-      omitExtraDecimalZeros: true,
-    });
-    const link = (
-      <DrilldownLink
-        ariaLabel={translateWithParameters(
-          'overview.see_more_details_on_x_of_y',
-          formattedValue,
-          localizeMetric(metricKey),
-        )}
-        branchLike={branchLike}
-        className="overview-measures-value text-light"
-        component={component.key}
-        metric={metricKey}
-      >
-        {formattedValue}
-      </DrilldownLink>
-    );
-    const label = this.getLabelText();
-
-    return centered ? (
-      <div className="display-flex-column flex-1">
-        <div className="display-flex-center display-flex-justify-center">
-          <span className="big-spacer-right">{icon}</span>
-          {link}
-        </div>
-        <div className="spacer-top text-center">{label}</div>
-      </div>
-    ) : (
-      <div className="display-flex-center">
-        <span className="big-spacer-right">{icon}</span>
-        <div className="display-flex-column">
-          <span>{link}</span>
-          <span className="spacer-top">{label}</span>
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx b/server/sonar-web/src/main/js/apps/overview/components/MetaTopBar.tsx
deleted file mode 100644 (file)
index bfe219a..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 React from 'react';
-import { useIntl } from 'react-intl';
-import DateFromNow from '../../../components/intl/DateFromNow';
-import { getLeakValue } from '../../../components/measure/utils';
-import { isPullRequest } from '../../../helpers/branch-like';
-import { findMeasure, formatMeasure } from '../../../helpers/measures';
-import { BranchLike } from '../../../types/branch-like';
-import { MetricKey, MetricType } from '../../../types/metrics';
-import { MeasureEnhanced } from '../../../types/types';
-
-interface Props {
-  branchLike: BranchLike;
-  measures: MeasureEnhanced[];
-}
-
-export default function MetaTopBar({ branchLike, measures }: Readonly<Props>) {
-  const intl = useIntl();
-  const isPR = isPullRequest(branchLike);
-
-  const leftSection = (
-    <div>
-      {isPR ? (
-        <>
-          <strong className="sw-body-sm-highlight sw-mr-1">
-            {formatMeasure(
-              getLeakValue(findMeasure(measures, MetricKey.new_lines)),
-              MetricType.ShortInteger,
-            ) ?? '0'}
-          </strong>
-          {intl.formatMessage({ id: 'metric.new_lines.name' })}
-        </>
-      ) : null}
-    </div>
-  );
-  const rightSection = (
-    <div>
-      {branchLike.analysisDate
-        ? intl.formatMessage(
-            {
-              id: 'overview.last_analysis_x',
-            },
-            {
-              date: (
-                <strong className="sw-body-sm-highlight">
-                  <DateFromNow date={branchLike.analysisDate} />
-                </strong>
-              ),
-            },
-          )
-        : null}
-    </div>
-  );
-
-  return (
-    <div className="sw-flex sw-justify-between sw-whitespace-nowrap sw-body-sm">
-      {leftSection}
-      {rightSection}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisErrorMessage-test.tsx
new file mode 100644 (file)
index 0000000..0958050
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
+import * as React from 'react';
+import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockTask } from '../../../../helpers/mocks/tasks';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../../types/features';
+import { AnalysisErrorMessage } from '../AnalysisErrorMessage';
+
+const handler = new BranchesServiceMock();
+
+beforeEach(() => {
+  handler.reset();
+});
+
+it('should work when error is on a different branch', () => {
+  renderAnalysisErrorMessage({
+    currentTask: mockTask({ branch: 'branch-1.2' }),
+  });
+
+  expect(screen.getByText(/component_navigation.status.failed_branch_X/)).toBeInTheDocument();
+  expect(screen.getByText(/branch-1\.2/)).toBeInTheDocument();
+});
+
+it('should work for errors on Pull Requests', async () => {
+  renderAnalysisErrorMessage(
+    {
+      currentTask: mockTask({ pullRequest: '01', pullRequestTitle: 'Fix stuff' }),
+    },
+    undefined,
+    'pullRequest=01&id=my-project',
+  );
+
+  expect(await screen.findByText(/component_navigation.status.failed_X/)).toBeInTheDocument();
+  expect(screen.getByText(/01 - Fix stuff/)).toBeInTheDocument();
+});
+
+it('should provide a link to admins', () => {
+  renderAnalysisErrorMessage({
+    component: mockComponent({ configuration: { showBackgroundTasks: true } }),
+  });
+
+  expect(screen.getByText(/component_navigation.status.failed_X.admin.link/)).toBeInTheDocument();
+  expect(screen.getByRole('link', { name: 'background_tasks.page' })).toBeInTheDocument();
+});
+
+it('should explain to admins how to get the staktrace', () => {
+  renderAnalysisErrorMessage(
+    {
+      component: mockComponent({ configuration: { showBackgroundTasks: true } }),
+    },
+    'project/background_tasks',
+  );
+
+  expect(screen.getByText(/component_navigation.status.failed_X.admin.help/)).toBeInTheDocument();
+  expect(screen.queryByRole('link', { name: 'background_tasks.page' })).not.toBeInTheDocument();
+});
+
+function renderAnalysisErrorMessage(
+  overrides: Partial<Parameters<typeof AnalysisErrorMessage>[0]> = {},
+  location = '/',
+  params?: string,
+) {
+  return renderApp(
+    location,
+    <AnalysisErrorMessage
+      component={mockComponent()}
+      currentTask={mockTask()}
+      onLeave={jest.fn()}
+      {...overrides}
+    />,
+    { navigateTo: params ? `/?${params}` : undefined, featureList: [Feature.BranchSupport] },
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisLicenseError-test.tsx
new file mode 100644 (file)
index 0000000..c5a8b5f
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
+import * as React from 'react';
+import { isValidLicense } from '../../../../api/editions';
+import { mockTask } from '../../../../helpers/mocks/tasks';
+import { mockAppState } from '../../../../helpers/testMocks';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { AnalysisLicenseError } from '../AnalysisLicenseError';
+
+jest.mock('../../../../api/editions', () => ({
+  isValidLicense: jest.fn().mockResolvedValue({ isValidLicense: true }),
+}));
+
+it('should handle a valid license', async () => {
+  renderAnalysisLicenseError({
+    currentTask: mockTask({ errorType: 'ANY_TYPE' }),
+  });
+
+  expect(
+    await screen.findByText(
+      'component_navigation.status.last_blocked_due_to_bad_license_X.qualifier.TRK',
+    ),
+  ).toBeInTheDocument();
+});
+
+it('should send user to contact the admin', async () => {
+  const errorMessage = 'error message';
+  renderAnalysisLicenseError({
+    currentTask: mockTask({ errorMessage, errorType: 'LICENSING_LOC' }),
+  });
+
+  expect(await screen.findByText('please_contact_administrator')).toBeInTheDocument();
+  expect(screen.getByText(errorMessage)).toBeInTheDocument();
+});
+
+it('should send provide a link to the admin', async () => {
+  jest.mocked(isValidLicense).mockResolvedValueOnce({ isValidLicense: false });
+
+  const errorMessage = 'error message';
+  renderAnalysisLicenseError(
+    {
+      currentTask: mockTask({ errorMessage, errorType: 'error-type' }),
+    },
+    true,
+  );
+
+  expect(
+    await screen.findByText('license.component_navigation.button.error-type.'),
+  ).toBeInTheDocument();
+  expect(screen.getByText(errorMessage)).toBeInTheDocument();
+});
+
+function renderAnalysisLicenseError(
+  overrides: Partial<Parameters<typeof AnalysisLicenseError>[0]> = {},
+  canAdmin = false,
+) {
+  return renderApp('/', <AnalysisLicenseError currentTask={mockTask()} {...overrides} />, {
+    appState: mockAppState({ canAdmin }),
+  });
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisStatus-it.tsx
new file mode 100644 (file)
index 0000000..921fc5c
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
+import ComputeEngineServiceMock from '../../../../api/mocks/ComputeEngineServiceMock';
+import { useComponent } from '../../../../app/components/componentContext/withComponentContext';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockTask, mockTaskWarning } from '../../../../helpers/mocks/tasks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { TaskStatuses } from '../../../../types/tasks';
+import { AnalysisStatus } from '../AnalysisStatus';
+
+const branchesHandler = new BranchesServiceMock();
+const handler = new ComputeEngineServiceMock();
+
+jest.mock('../../../../app/components/componentContext/withComponentContext', () => ({
+  useComponent: jest.fn(() => ({
+    isInProgress: true,
+    isPending: false,
+    currentTask: mockTask(),
+    component: mockComponent(),
+  })),
+}));
+
+beforeEach(() => {
+  branchesHandler.reset();
+  handler.reset();
+});
+
+it('renders correctly when there is a background task in progress', () => {
+  renderAnalysisStatus();
+  expect(
+    screen.getByText('project_navigation.analysis_status.in_progress', { exact: false }),
+  ).toBeInTheDocument();
+});
+
+it('renders correctly when there is a background task pending', () => {
+  jest.mocked(useComponent).mockReturnValue({
+    isInProgress: false,
+    isPending: true,
+    currentTask: mockTask(),
+    onComponentChange: jest.fn(),
+    fetchComponent: jest.fn(),
+  });
+  renderAnalysisStatus();
+  expect(
+    screen.getByText('project_navigation.analysis_status.pending', { exact: false }),
+  ).toBeInTheDocument();
+});
+
+it('renders correctly when there is a failing background task', () => {
+  jest.mocked(useComponent).mockReturnValue({
+    isInProgress: false,
+    isPending: false,
+    currentTask: mockTask({ status: TaskStatuses.Failed }),
+    onComponentChange: jest.fn(),
+    fetchComponent: jest.fn(),
+  });
+  renderAnalysisStatus();
+  expect(
+    screen.getByText('project_navigation.analysis_status.failed', { exact: false }),
+  ).toBeInTheDocument();
+});
+
+it('renders correctly when there are analysis warnings', async () => {
+  const user = userEvent.setup();
+  jest.mocked(useComponent).mockReturnValue({
+    isInProgress: false,
+    isPending: false,
+    currentTask: mockTask(),
+    onComponentChange: jest.fn(),
+    fetchComponent: jest.fn(),
+  });
+  handler.setTaskWarnings([mockTaskWarning({ message: 'warning 1' })]);
+  renderAnalysisStatus();
+
+  await user.click(await screen.findByText('project_navigation.analysis_status.details_link'));
+  expect(screen.getByText('warning 1')).toBeInTheDocument();
+  await user.click(screen.getByText('close'));
+  expect(screen.queryByText('warning 1')).not.toBeInTheDocument();
+});
+
+function renderAnalysisStatus(overrides: Partial<Parameters<typeof AnalysisStatus>[0]> = {}) {
+  return renderComponent(
+    <AnalysisStatus component={mockComponent()} {...overrides} />,
+    '/?id=my-project',
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/AnalysisWarningsModal-test.tsx
new file mode 100644 (file)
index 0000000..4f3e735
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
+import * as React from 'react';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockTaskWarning } from '../../../../helpers/mocks/tasks';
+import { mockCurrentUser } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ComponentPropsType } from '../../../../helpers/testUtils';
+import { AnalysisWarningsModal } from '../AnalysisWarningsModal';
+
+jest.mock('../../../../api/ce', () => ({
+  dismissAnalysisWarning: jest.fn().mockResolvedValue(null),
+  getTask: jest.fn().mockResolvedValue({
+    warnings: ['message foo', 'message-bar', 'multiline message\nsecondline\n  third line'],
+  }),
+}));
+
+beforeEach(jest.clearAllMocks);
+
+describe('should render correctly', () => {
+  it('should not show dismiss buttons for non-dismissable warnings', () => {
+    renderAnalysisWarningsModal();
+
+    expect(screen.getByText('warning 1')).toBeInTheDocument();
+    expect(screen.getByText('warning 2')).toBeInTheDocument();
+    expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument();
+  });
+
+  it('should show a dismiss button for dismissable warnings', () => {
+    renderAnalysisWarningsModal({ warnings: [mockTaskWarning({ dismissable: true })] });
+
+    expect(screen.getByRole('button', { name: 'dismiss_permanently' })).toBeInTheDocument();
+  });
+
+  it('should not show dismiss buttons if not logged in', () => {
+    renderAnalysisWarningsModal({
+      currentUser: mockCurrentUser({ isLoggedIn: false }),
+      warnings: [mockTaskWarning({ dismissable: true })],
+    });
+
+    expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument();
+  });
+});
+
+function renderAnalysisWarningsModal(
+  props: Partial<ComponentPropsType<typeof AnalysisWarningsModal>> = {},
+) {
+  return renderComponent(
+    <AnalysisWarningsModal
+      component={mockComponent()}
+      currentUser={mockCurrentUser({ isLoggedIn: true })}
+      onClose={jest.fn()}
+      warnings={[
+        mockTaskWarning({ message: 'warning 1' }),
+        mockTaskWarning({ message: 'warning 2' }),
+      ]}
+      {...props}
+    />,
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/MeasurementLabel-test.tsx
deleted file mode 100644 (file)
index 5453149..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
-import * as React from 'react';
-import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
-import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { MetricKey } from '../../../../types/metrics';
-import { MeasurementType } from '../../utils';
-import MeasurementLabel from '../MeasurementLabel';
-
-it('should render correctly for coverage', async () => {
-  renderMeasurementLabel();
-  expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument();
-
-  renderMeasurementLabel({
-    measures: [
-      mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.coverage }) }),
-      mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.lines_to_cover }) }),
-    ],
-  });
-  expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument();
-  expect(await screen.findByText('overview.coverage_on_X_lines')).toBeInTheDocument();
-
-  renderMeasurementLabel({
-    measures: [
-      mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_coverage }) }),
-      mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_lines_to_cover }) }),
-    ],
-    useDiffMetric: true,
-  });
-  expect(screen.getByRole('link', { name: /.*new_coverage.*/ })).toBeInTheDocument();
-  expect(await screen.findByText('overview.coverage_on_X_lines')).toBeInTheDocument();
-  expect(await screen.findByText('overview.coverage_on_X_new_lines')).toBeInTheDocument();
-});
-
-it('should render correctly for duplications', async () => {
-  renderMeasurementLabel({
-    measures: [
-      mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.duplicated_lines_density }) }),
-    ],
-    type: MeasurementType.Duplication,
-  });
-  expect(
-    screen.getByRole('link', {
-      name: 'overview.see_more_details_on_x_of_y.1.0%.metric.duplicated_lines_density.name',
-    }),
-  ).toBeInTheDocument();
-  expect(await screen.findByText('metric.duplicated_lines_density.short_name')).toBeInTheDocument();
-
-  renderMeasurementLabel({
-    measures: [
-      mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.duplicated_lines_density }) }),
-      mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.ncloc }) }),
-    ],
-    type: MeasurementType.Duplication,
-  });
-  expect(await screen.findByText('metric.duplicated_lines_density.short_name')).toBeInTheDocument();
-  expect(await screen.findByText('overview.duplications_on_X_lines')).toBeInTheDocument();
-
-  renderMeasurementLabel({
-    measures: [
-      mockMeasureEnhanced({
-        metric: mockMetric({ key: MetricKey.new_duplicated_lines_density }),
-      }),
-      mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_lines }) }),
-    ],
-    type: MeasurementType.Duplication,
-    useDiffMetric: true,
-  });
-
-  expect(
-    screen.getByRole('link', {
-      name: 'overview.see_more_details_on_x_of_y.1.0%.metric.new_duplicated_lines_density.name',
-    }),
-  ).toBeInTheDocument();
-  expect(await screen.findByText('overview.duplications_on_X_new_lines')).toBeInTheDocument();
-});
-
-it('should render correctly with no value', async () => {
-  renderMeasurementLabel({ measures: [] });
-  expect(await screen.findByText('metric.coverage.name')).toBeInTheDocument();
-});
-
-function renderMeasurementLabel(props: Partial<MeasurementLabel['props']> = {}) {
-  return renderComponent(
-    <MeasurementLabel
-      branchLike={mockPullRequest()}
-      component={mockComponent()}
-      measures={[mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.coverage }) })]}
-      type={MeasurementType.Coverage}
-      {...props}
-    />,
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts b/server/sonar-web/src/main/js/apps/overview/components/useLicenseIsValid.ts
new file mode 100644 (file)
index 0000000..8693d6b
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 React, { useEffect } from 'react';
+import { isValidLicense } from '../../../api/editions';
+
+export function useLicenseIsValid(): [boolean, boolean] {
+  const [licenseIsValid, setLicenseIsValid] = React.useState(false);
+  const [loading, setLoading] = React.useState(true);
+
+  useEffect(() => {
+    setLoading(true);
+
+    isValidLicense()
+      .then(({ isValidLicense }) => {
+        setLicenseIsValid(isValidLicense);
+        setLoading(false);
+      })
+      .catch(() => {
+        setLoading(false);
+      });
+  }, []);
+
+  return [licenseIsValid, loading];
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestMetaTopBar.tsx
new file mode 100644 (file)
index 0000000..72c4a6a
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { SeparatorCircleIcon } from 'design-system';
+import React from 'react';
+import { useIntl } from 'react-intl';
+import CurrentBranchLikeMergeInformation from '../../../app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation';
+import DateFromNow from '../../../components/intl/DateFromNow';
+import { getLeakValue } from '../../../components/measure/utils';
+import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { PullRequest } from '../../../types/branch-like';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { MeasureEnhanced } from '../../../types/types';
+
+interface Props {
+  pullRequest: PullRequest;
+  measures: MeasureEnhanced[];
+}
+
+export default function PullRequestMetaTopBar({ pullRequest, measures }: Readonly<Props>) {
+  const intl = useIntl();
+
+  const leftSection = (
+    <div>
+      <strong className="sw-body-sm-highlight sw-mr-1">
+        {formatMeasure(
+          getLeakValue(findMeasure(measures, MetricKey.new_lines)),
+          MetricType.ShortInteger,
+        ) || '0'}
+      </strong>
+      {intl.formatMessage({ id: 'metric.new_lines.name' })}
+    </div>
+  );
+  const rightSection = (
+    <div className="sw-flex sw-items-center sw-gap-2">
+      <CurrentBranchLikeMergeInformation pullRequest={pullRequest} />
+
+      {pullRequest.analysisDate && (
+        <>
+          <SeparatorCircleIcon />
+          {intl.formatMessage(
+            {
+              id: 'overview.last_analysis_x',
+            },
+            {
+              date: (
+                <strong className="sw-body-sm-highlight">
+                  <DateFromNow date={pullRequest.analysisDate} />
+                </strong>
+              ),
+            },
+          )}
+        </>
+      )}
+    </div>
+  );
+
+  return (
+    <div className="sw-flex sw-justify-between sw-whitespace-nowrap sw-body-sm">
+      {leftSection}
+      {rightSection}
+    </div>
+  );
+}
index 3de4a392362970979b62bb4915fc48b08b8a42af..5e7c411c033bcf28586a54bd6794a3281a7f12bb 100644 (file)
@@ -28,13 +28,14 @@ import { useComponentMeasuresWithMetricsQuery } from '../../../queries/component
 import { useComponentQualityGateQuery } from '../../../queries/quality-gates';
 import { PullRequest } from '../../../types/branch-like';
 import { Component } from '../../../types/types';
+import { AnalysisStatus } from '../components/AnalysisStatus';
 import BranchQualityGate from '../components/BranchQualityGate';
 import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
-import MetaTopBar from '../components/MetaTopBar';
 import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide';
 import '../styles.css';
 import { PR_METRICS, Status } from '../utils';
 import MeasuresCardPanel from './MeasuresCardPanel';
+import PullRequestMetaTopBar from './PullRequestMetaTopBar';
 import SonarLintAd from './SonarLintAd';
 
 interface Props {
@@ -97,9 +98,11 @@ export default function PullRequestOverview(props: Readonly<Readonly<Props>>) {
     <CenteredLayout>
       <PageContentFontWrapper className="it__pr-overview sw-mt-12 sw-mb-8 sw-grid sw-grid-cols-12 sw-body-sm">
         <div className="sw-col-start-2 sw-col-span-10">
-          <MetaTopBar branchLike={pullRequest} measures={measures} />
+          <PullRequestMetaTopBar pullRequest={pullRequest} measures={measures} />
           <BasicSeparator className="sw-my-4" />
 
+          <AnalysisStatus className="sw-mb-4" component={component} />
+
           {ignoredConditions && <IgnoredConditionWarning />}
 
           {status && (
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
deleted file mode 100644 (file)
index 33f0295..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
-import * as React from 'react';
-import { AnalysisWarningsModal } from '../../../app/components/nav/component/AnalysisWarningsModal';
-import { mockComponent } from '../../../helpers/mocks/component';
-import { mockTaskWarning } from '../../../helpers/mocks/tasks';
-import { mockCurrentUser } from '../../../helpers/testMocks';
-import { renderComponent } from '../../../helpers/testReactTestingUtils';
-import { ComponentPropsType } from '../../../helpers/testUtils';
-
-jest.mock('../../../api/ce', () => ({
-  dismissAnalysisWarning: jest.fn().mockResolvedValue(null),
-  getTask: jest.fn().mockResolvedValue({
-    warnings: ['message foo', 'message-bar', 'multiline message\nsecondline\n  third line'],
-  }),
-}));
-
-beforeEach(jest.clearAllMocks);
-
-describe('should render correctly', () => {
-  it('should not show dismiss buttons for non-dismissable warnings', () => {
-    renderAnalysisWarningsModal();
-
-    expect(screen.getByText('warning 1')).toBeInTheDocument();
-    expect(screen.getByText('warning 2')).toBeInTheDocument();
-    expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument();
-  });
-
-  it('should show a dismiss button for dismissable warnings', () => {
-    renderAnalysisWarningsModal({ warnings: [mockTaskWarning({ dismissable: true })] });
-
-    expect(screen.getByRole('button', { name: 'dismiss_permanently' })).toBeInTheDocument();
-  });
-
-  it('should not show dismiss buttons if not logged in', () => {
-    renderAnalysisWarningsModal({
-      currentUser: mockCurrentUser({ isLoggedIn: false }),
-      warnings: [mockTaskWarning({ dismissable: true })],
-    });
-
-    expect(screen.queryByRole('button', { name: 'dismiss_permanently' })).not.toBeInTheDocument();
-  });
-});
-
-function renderAnalysisWarningsModal(
-  props: Partial<ComponentPropsType<typeof AnalysisWarningsModal>> = {},
-) {
-  return renderComponent(
-    <AnalysisWarningsModal
-      component={mockComponent()}
-      currentUser={mockCurrentUser({ isLoggedIn: true })}
-      onClose={jest.fn()}
-      warnings={[
-        mockTaskWarning({ message: 'warning 1' }),
-        mockTaskWarning({ message: 'warning 2' }),
-      ]}
-      {...props}
-    />,
-  );
-}
index 8803258c32da94b4dc22dcf7e0b97efa65b26ab0..e7d6e2ff1af73760cd181259c9ded203c9aa69ea 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 { DiscreetInteractiveIcon, HomeFillIcon, HomeIcon } from 'design-system';
-import * as React from 'react';
+import { ButtonSecondary, DiscreetInteractiveIcon, HomeFillIcon, HomeIcon } from 'design-system';
+import React from 'react';
+import { useIntl } from 'react-intl';
 import { setHomePage } from '../../api/users';
 import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
 import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
-import { translate } from '../../helpers/l10n';
 import { isSameHomePage } from '../../helpers/users';
 import { HomePage, isLoggedIn } from '../../types/users';
 import Tooltip from './Tooltip';
@@ -31,55 +31,62 @@ interface Props
   extends Pick<CurrentUserContextInterface, 'currentUser' | 'updateCurrentUserHomepage'> {
   className?: string;
   currentPage: HomePage;
+  type?: 'button' | 'icon';
 }
 
 export const DEFAULT_HOMEPAGE: HomePage = { type: 'PROJECTS' };
 
-export class HomePageSelect extends React.PureComponent<Props> {
-  async setCurrentUserHomepage(homepage: HomePage) {
-    const { currentUser } = this.props;
+export function HomePageSelect(props: Readonly<Props>) {
+  const { currentPage, className, type = 'icon', currentUser, updateCurrentUserHomepage } = props;
+  const intl = useIntl();
 
+  if (!isLoggedIn(currentUser)) {
+    return null;
+  }
+
+  const isChecked =
+    currentUser.homepage !== undefined && isSameHomePage(currentUser.homepage, currentPage);
+  const isDefault = isChecked && isSameHomePage(currentPage, DEFAULT_HOMEPAGE);
+
+  const setCurrentUserHomepage = async (homepage: HomePage) => {
     if (isLoggedIn(currentUser)) {
       await setHomePage(homepage);
 
-      this.props.updateCurrentUserHomepage(homepage);
+      updateCurrentUserHomepage(homepage);
     }
-  }
-
-  handleClick = () => {
-    this.setCurrentUserHomepage(this.props.currentPage);
-  };
-
-  handleReset = () => {
-    this.setCurrentUserHomepage(DEFAULT_HOMEPAGE);
   };
 
-  render() {
-    const { className, currentPage, currentUser } = this.props;
+  const tooltip = isChecked
+    ? intl.formatMessage({ id: isDefault ? 'homepage.current.is_default' : 'homepage.current' })
+    : intl.formatMessage({ id: 'homepage.check' });
 
-    if (!isLoggedIn(currentUser)) {
-      return null;
-    }
+  const handleClick = () => setCurrentUserHomepage?.(isChecked ? DEFAULT_HOMEPAGE : currentPage);
 
-    const { homepage } = currentUser;
-    const isChecked = homepage !== undefined && isSameHomePage(homepage, currentPage);
-    const isDefault = isChecked && isSameHomePage(currentPage, DEFAULT_HOMEPAGE);
-    const tooltip = isChecked
-      ? translate(isDefault ? 'homepage.current.is_default' : 'homepage.current')
-      : translate('homepage.check');
+  const Icon = isChecked ? HomeFillIcon : HomeIcon;
 
-    return (
-      <Tooltip overlay={tooltip}>
+  return (
+    <Tooltip overlay={tooltip}>
+      {type === 'icon' ? (
         <DiscreetInteractiveIcon
           aria-label={tooltip}
           className={className}
           disabled={isDefault}
-          Icon={isChecked ? HomeFillIcon : HomeIcon}
-          onClick={isChecked ? this.handleReset : this.handleClick}
+          Icon={Icon}
+          onClick={handleClick}
         />
-      </Tooltip>
-    );
-  }
+      ) : (
+        <ButtonSecondary
+          aria-label={tooltip}
+          icon={<Icon />}
+          className={className}
+          disabled={isDefault}
+          onClick={handleClick}
+        >
+          {intl.formatMessage({ id: 'overview.set_as_homepage' })}
+        </ButtonSecondary>
+      )}
+    </Tooltip>
+  );
 }
 
 export default withCurrentUserContext(HomePageSelect);
index 87f65cdda54e3ee7906e5435d89b72bca3afb181..e3ef11db92c3ed03f751ea3a88df64d456593d07 100644 (file)
@@ -23,6 +23,7 @@ import * as React from 'react';
 import { setHomePage } from '../../../api/users';
 import { mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { FCProps } from '../../../types/misc';
 import { DEFAULT_HOMEPAGE, HomePageSelect } from '../HomePageSelect';
 
 jest.mock('../../../api/users', () => ({
@@ -56,7 +57,7 @@ it('renders correctly if user is on the homepage', async () => {
   expect(button).toHaveFocus();
 });
 
-function renderHomePageSelect(props: Partial<HomePageSelect['props']> = {}) {
+function renderHomePageSelect(props: Partial<FCProps<typeof HomePageSelect>> = {}) {
   return renderComponent(
     <HomePageSelect
       currentPage={{ type: 'MY_PROJECTS' }}
index 07b28dccafd846a006d5bb1847f15d2f327949f1..b84f6b21849cc96619ef5d8312e286a1ef57648c 100644 (file)
@@ -28,6 +28,7 @@ import {
   Title,
 } from 'design-system';
 import * as React from 'react';
+import { AnalysisStatus } from '../../apps/overview/components/AnalysisStatus';
 import { isMainBranch } from '../../helpers/branch-like';
 import { translate } from '../../helpers/l10n';
 import { getBaseUrl } from '../../helpers/system';
@@ -129,12 +130,15 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
 
   return (
     <div className="sw-body-sm">
+      <AnalysisStatus component={component} className="sw-mb-4 sw-w-max" />
+
       {selectedTutorial === undefined && (
         <div className="sw-flex sw-flex-col">
           <Title className="sw-mb-6 sw-heading-lg">
             {translate('onboarding.tutorial.page.title')}
           </Title>
           <LightPrimary>{translate('onboarding.tutorial.page.description')}</LightPrimary>
+
           <SubTitle className="sw-mt-12 sw-mb-4 sw-heading-md">
             {translate('onboarding.tutorial.choose_method')}
           </SubTitle>
index b177ff6b9a58cf606c77add30b4bb9e8c13ea8de..7b02bac6fb1288a2901c374686c8da8765d9b375 100644 (file)
@@ -103,7 +103,11 @@ export function renderAppWithAdminContext(
 export function renderComponent(
   component: React.ReactElement,
   pathname = '/',
-  { appState = mockAppState(), featureList = [] }: RenderContext = {},
+  {
+    appState = mockAppState(),
+    featureList = [],
+    currentUser = mockCurrentUser(),
+  }: RenderContext = {},
 ) {
   function Wrapper({ children }: { children: React.ReactElement }) {
     const queryClient = new QueryClient();
@@ -113,13 +117,15 @@ export function renderComponent(
         <QueryClientProvider client={queryClient}>
           <HelmetProvider>
             <AvailableFeaturesContext.Provider value={featureList}>
-              <AppStateContextProvider appState={appState}>
-                <MemoryRouter initialEntries={[pathname]}>
-                  <Routes>
-                    <Route path="*" element={children} />
-                  </Routes>
-                </MemoryRouter>
-              </AppStateContextProvider>
+              <CurrentUserContextProvider currentUser={currentUser}>
+                <AppStateContextProvider appState={appState}>
+                  <MemoryRouter initialEntries={[pathname]}>
+                    <Routes>
+                      <Route path="*" element={children} />
+                    </Routes>
+                  </MemoryRouter>
+                </AppStateContextProvider>
+              </CurrentUserContextProvider>
             </AvailableFeaturesContext.Provider>
           </HelmetProvider>
         </QueryClientProvider>
index 4a9e7fa67e835d969045b2da8933d8f359b0b17a..416034aba9be26ba8d0483ee7243f02bf37d08b1 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 { Task } from './tasks';
 import { Component, LightComponent } from './types';
 
 export enum Visibility {
@@ -96,6 +97,7 @@ export function isView(
 
 export interface ComponentContextShape {
   component?: Component;
+  currentTask?: Task;
   isInProgress?: boolean;
   isPending?: boolean;
   onComponentChange: (changes: Partial<Component>) => void;
index 5aba25d721dade0ff5fced1f6ff7ce2150928650..58ddb11c238acfc270d9fceb5996f08e7f50b0e4 100644 (file)
@@ -3895,6 +3895,7 @@ overview.quality_profiles=Quality Profiles used
 overview.new_code_period_x=New Code: {0}
 overview.max_new_code_period_from_x=Max New Code from: {0}
 overview.started_x=Started {0}
+overview.set_as_homepage=Set as homepage
 overview.new_code=New Code
 overview.overall_code=Overall Code
 overview.last_analysis_x=Last analysis {date}