]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19613 Move Project Info from sidebar to separate page
authorViktor Vorona <viktor.vorona@sonarsource.com>
Mon, 19 Jun 2023 11:22:59 +0000 (13:22 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Jun 2023 20:03:55 +0000 (20:03 +0000)
65 files changed:
server/sonar-web/design-system/src/components/Title.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/DrawerLink.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.css [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawerPage.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.css [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationPages.ts [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationRenderer.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeButton.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeParams.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/styles.css [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaKey.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLink.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLinks.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaSize.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTags.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTagsSelector.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaKey-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaTags-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/RegulatoryReport.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/RegulatoryReportModal.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/__tests__/RegulatoryReport-it.tsx [deleted file]
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeParams.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/utils-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/badges/styles.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/badges/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaKey.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLink.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLinks.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityGate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityProfiles.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaSize.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTags.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTagsSelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaKey-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaTags-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/notifications/ProjectNotifications.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/RegulatoryReport.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/RegulatoryReportModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/__tests__/RegulatoryReport-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/routes.tsx [new file with mode: 0644]

diff --git a/server/sonar-web/design-system/src/components/Title.tsx b/server/sonar-web/design-system/src/components/Title.tsx
new file mode 100644 (file)
index 0000000..3e4b8d2
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 styled from '@emotion/styled';
+import tw from 'twin.macro';
+import { themeColor } from '../helpers/theme';
+
+export const Title = styled.h1`
+  ${tw`sw-heading-lg`}
+  ${tw`sw-mb-4`}
+  color: ${themeColor('pageTitle')};
+`;
+
+export const SubTitle = styled.h2`
+  ${tw`sw-heading-md`}
+  ${tw`sw-mb-4`}
+  color: ${themeColor('pageTitle')};
+`;
+
+export const SubHeading = styled.h3`
+  ${tw`sw-body-md-highlight`}
+  ${tw`sw-mb-2`}
+  color: ${themeColor('pageContent')};
+`;
+
+export const SubHeadingHighlight = styled(SubHeading)`
+  color: ${themeColor('pageContentDark')};
+`;
index c8d59d982b1517ffbd5458c8406a2da7be872b46..ba3cb3bc20a66f187f89fe5fe8b8d5e241823224 100644 (file)
@@ -81,6 +81,7 @@ export * from './Table';
 export * from './Tags';
 export * from './TagsSelector';
 export * from './Text';
+export * from './Title';
 export { ToggleButton } from './ToggleButton';
 export { TopBar } from './TopBar';
 export * from './TreeMap';
index bddd03d7ae909c8b7541bc36171064bd5745572c..0f01470dd74a52a2e4372d017bd53def96effcd7 100644 (file)
@@ -46,11 +46,11 @@ import { Feature } from '../../types/features';
 import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks';
 import { Component, Status } from '../../types/types';
 import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
+import ComponentContainerNotFound from './ComponentContainerNotFound';
 import withAvailableFeatures, {
   WithAvailableFeaturesProps,
 } from './available-features/withAvailableFeatures';
 import withBranchStatusActions from './branch-status/withBranchStatusActions';
-import ComponentContainerNotFound from './ComponentContainerNotFound';
 import { ComponentContext } from './componentContext/ComponentContext';
 import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation';
 import ComponentNav from './nav/component/ComponentNav';
@@ -456,7 +456,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
               currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)}
               isInProgress={isInProgress}
               isPending={isPending}
-              onComponentChange={this.handleComponentChange}
               onWarningDismiss={this.handleWarningDismiss}
               projectBinding={projectBinding}
               projectBindingErrors={projectBindingErrors}
index 98c59386a469615224ec7bcada4b7b48ca309b20..361f3d68d437acb6b10c986d877adb117fa9ff10 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 { LAYOUT_GLOBAL_NAV_HEIGHT, LAYOUT_PROJECT_NAV_HEIGHT, TopBar } from 'design-system';
+import { TopBar } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../../helpers/l10n';
 import {
@@ -33,8 +33,6 @@ import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingEr
 import Header from './Header';
 import HeaderMeta from './HeaderMeta';
 import Menu from './Menu';
-import InfoDrawer from './projectInformation/InfoDrawer';
-import ProjectInformation from './projectInformation/ProjectInformation';
 
 export interface ComponentNavProps {
   branchLikes: BranchLike[];
@@ -44,7 +42,6 @@ export interface ComponentNavProps {
   currentTaskOnSameBranch?: boolean;
   isInProgress?: boolean;
   isPending?: boolean;
-  onComponentChange: (changes: Partial<Component>) => void;
   onWarningDismiss: () => void;
   projectBinding?: ProjectAlmBindingResponse;
   projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
@@ -119,19 +116,6 @@ export default function ComponentNav(props: ComponentNavProps) {
           }}
           projectInfoDisplayed={displayProjectInfo}
         />
-        <InfoDrawer
-          displayed={displayProjectInfo}
-          onClose={() => {
-            setDisplayProjectInfo(false);
-          }}
-          top={LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_PROJECT_NAV_HEIGHT}
-        >
-          <ProjectInformation
-            branchLike={currentBranchLike}
-            component={component}
-            onComponentChange={props.onComponentChange}
-          />
-        </InfoDrawer>
       </TopBar>
       {prDecoNotifComponent}
     </>
index 3806988aa999abb57d0bc3a3d73c97f9e70817a9..bab7b4e3e5dababa687e744ac859dd5b6feda565 100644 (file)
@@ -342,6 +342,7 @@ export class Menu extends React.PureComponent<Props> {
     const isApplication = this.isApplication();
     const label = translate(isProject ? 'project' : 'application', 'info.title');
     const isApplicationChildInaccessble = this.isApplicationChildInaccessble();
+    const query = this.getQuery();
 
     if (isPullRequest(this.props.branchLike)) {
       return null;
@@ -355,10 +356,8 @@ export class Menu extends React.PureComponent<Props> {
       (isProject || isApplication) && (
         <li className="sw-body-md sw-pb-4">
           <Link
-            onClick={this.props.onToggleProjectInfo}
-            preventDefault
             ref={(node: HTMLAnchorElement | null) => (this.projectInfoLink = node)}
-            to={{}}
+            to={{ pathname: '/project/info', search: new URLSearchParams(query).toString() }}
           >
             {label}
           </Link>
index 18c7a3353f229ea36c0b9e9a1d2f8fb0a949627b..35ad1906cb9cd0864955dc1c9f0c7b80d8cf901c 100644 (file)
@@ -67,10 +67,7 @@ it('renders correctly when the project binding is incorrect', () => {
 it('correctly returns focus to the Project Information link when the drawer is closed', () => {
   renderComponentNav();
   screen.getByRole('link', { name: 'project.info.title' }).click();
-  expect(screen.getByRole('link', { name: 'project.info.title' })).not.toHaveFocus();
-
-  screen.getByRole('button', { name: 'close' }).click();
-  expect(screen.getByRole('link', { name: 'project.info.title' })).toHaveFocus();
+  expect(screen.getByText('/project/info?id=my-project')).toBeInTheDocument();
 });
 
 function renderComponentNav(props: Partial<ComponentNavProps> = {}) {
@@ -84,7 +81,6 @@ function renderComponentNav(props: Partial<ComponentNavProps> = {}) {
       currentBranchLike={undefined}
       isInProgress={false}
       isPending={false}
-      onComponentChange={jest.fn()}
       onWarningDismiss={jest.fn()}
       warnings={[]}
       {...props}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/DrawerLink.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/DrawerLink.tsx
deleted file mode 100644 (file)
index ea4faa6..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 ChevronRightIcon from '../../../../../components/icons/ChevronRightIcon';
-
-export interface DrawerLinkProps<P> {
-  label: string;
-  onPageChange: (page: P) => void;
-  to: P;
-}
-
-export function DrawerLink<P>(props: DrawerLinkProps<P>) {
-  const { label, to } = props;
-
-  return (
-    <a
-      className="display-flex-space-between bordered-bottom big-padded"
-      onClick={() => props.onPageChange(to)}
-      role="link"
-      tabIndex={0}
-    >
-      {label}
-      <ChevronRightIcon />
-    </a>
-  );
-}
-
-export default React.memo(DrawerLink);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.css b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.css
deleted file mode 100644 (file)
index 1070a3d..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.
- */
-:root {
-  --drawer-width: 380px;
-}
-
-.info-drawer-pane {
-  background-color: white;
-  right: calc(-1 * var(--drawer-width));
-  width: var(--drawer-width);
-  transition: right 0.3s ease-in-out;
-  border-top: 1px solid var(--barBorderColor);
-  border-left: 1px solid var(--barBorderColor);
-  box-sizing: border-box;
-}
-
-.info-drawer-pane.open {
-  right: 0;
-}
-
-.info-drawer {
-  position: fixed;
-  /* top is defined programmatically by ComponentNav */
-  bottom: 0;
-  z-index: var(--pageSideZIndex);
-}
-
-.info-drawer .close-button {
-  position: absolute;
-  top: 0;
-  right: 0;
-  background: white;
-  padding: calc(2 * var(--gridSize));
-  z-index: var(--normalZIndex);
-}
-
-.info-drawer .back-button {
-  cursor: pointer;
-}
-
-.info-drawer .back-button:hover {
-  color: var(--blue);
-}
-
-.info-drawer-page {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawer.tsx
deleted file mode 100644 (file)
index 8edd77b..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 * as React from 'react';
-import { ClearButton } from '../../../../../components/controls/buttons';
-import EscKeydownHandler from '../../../../../components/controls/EscKeydownHandler';
-import OutsideClickHandler from '../../../../../components/controls/OutsideClickHandler';
-import { translate } from '../../../../../helpers/l10n';
-import './InfoDrawer.css';
-
-export interface InfoDrawerProps {
-  children: React.ReactNode;
-  displayed: boolean;
-  onClose: () => void;
-  top: number;
-}
-
-export default function InfoDrawer(props: InfoDrawerProps) {
-  const { children, displayed, onClose, top } = props;
-
-  return (
-    <div
-      className={classNames('info-drawer info-drawer-pane', { open: displayed })}
-      style={{ top }}
-    >
-      {displayed && (
-        <>
-          <div className="close-button">
-            <ClearButton aria-label={translate('close')} onClick={onClose} />
-          </div>
-          <EscKeydownHandler onKeydown={onClose}>
-            <OutsideClickHandler onClickOutside={onClose}>
-              <div className="display-flex-column max-height-100">{children}</div>
-            </OutsideClickHandler>
-          </EscKeydownHandler>
-        </>
-      )}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawerPage.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/InfoDrawerPage.tsx
deleted file mode 100644 (file)
index 072a05f..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 * as React from 'react';
-import BackIcon from '../../../../../components/icons/BackIcon';
-import { translate } from '../../../../../helpers/l10n';
-
-export interface InfoDrawerPageProps {
-  children: React.ReactNode;
-  displayed: boolean;
-  onPageChange: () => void;
-}
-
-export default function InfoDrawerPage(props: InfoDrawerPageProps) {
-  const { children, displayed, onPageChange } = props;
-  return (
-    <div
-      className={classNames(
-        'info-drawer-page info-drawer-pane display-flex-column overflow-hidden',
-        {
-          open: displayed,
-        }
-      )}
-    >
-      {displayed && (
-        <>
-          <a
-            className="h2 back-button big-padded bordered-bottom"
-            onClick={() => onPageChange()}
-            role="link"
-            tabIndex={-1}
-          >
-            <BackIcon className="little-spacer-right" />
-            {translate('back')}
-          </a>
-          <div className="overflow-y-auto big-padded">{children}</div>
-        </>
-      )}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.css b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.css
deleted file mode 100644 (file)
index 4a86ab5..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.
- */
-.project-info-list > li {
-  /* 1px to not cut icons on the left */
-  padding-left: 1px;
-  padding-bottom: 4px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.project-info-tags {
-  position: relative;
-}
-
-.project-info-deleted-profile,
-.project-info-deprecated-rules {
-  margin: 4px -6px 4px;
-  padding: 3px 6px !important;
-  border: 1px solid var(--alertBorderError);
-  border-radius: 3px;
-  background-color: var(--alertBackgroundError);
-}
-
-.project-info-deleted-profile a,
-.project-info-deprecated-rules a {
-  color: var(--veryDarkBlue);
-  border-color: darken(var(--lightBlue));
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx
deleted file mode 100644 (file)
index 2b02e44..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { getMeasures } from '../../../../../api/measures';
-import { BranchLike } from '../../../../../types/branch-like';
-import { ComponentQualifier } from '../../../../../types/component';
-import { MetricKey } from '../../../../../types/metrics';
-import { Component, Dict, Measure, Metric } from '../../../../../types/types';
-import { CurrentUser, isLoggedIn } from '../../../../../types/users';
-import withCurrentUserContext from '../../../current-user/withCurrentUserContext';
-import withMetricsContext from '../../../metrics/withMetricsContext';
-import ProjectBadges from './badges/ProjectBadges';
-import InfoDrawerPage from './InfoDrawerPage';
-import ProjectNotifications from './notifications/ProjectNotifications';
-import './ProjectInformation.css';
-import { ProjectInformationPages } from './ProjectInformationPages';
-import ProjectInformationRenderer from './ProjectInformationRenderer';
-
-interface Props {
-  branchLike?: BranchLike;
-  component: Component;
-  currentUser: CurrentUser;
-  onComponentChange: (changes: {}) => void;
-  metrics: Dict<Metric>;
-}
-
-interface State {
-  measures?: Measure[];
-  page: ProjectInformationPages;
-}
-
-export class ProjectInformation extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = {
-    page: ProjectInformationPages.main,
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadMeasures();
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  setPage = (page: ProjectInformationPages = ProjectInformationPages.main) => {
-    this.setState({ page });
-  };
-
-  loadMeasures = () => {
-    const {
-      component: { key },
-    } = this.props;
-
-    return getMeasures({
-      component: key,
-      metricKeys: [MetricKey.ncloc, MetricKey.projects].join(),
-    }).then((measures) => {
-      if (this.mounted) {
-        this.setState({ measures });
-      }
-    });
-  };
-
-  render() {
-    const { branchLike, component, currentUser, metrics } = this.props;
-    const { measures, page } = this.state;
-
-    const canConfigureNotifications =
-      isLoggedIn(currentUser) && component.qualifier === ComponentQualifier.Project;
-    const canUseBadges =
-      metrics !== undefined &&
-      (component.qualifier === ComponentQualifier.Application ||
-        component.qualifier === ComponentQualifier.Project);
-
-    return (
-      <>
-        <ProjectInformationRenderer
-          canConfigureNotifications={canConfigureNotifications}
-          canUseBadges={canUseBadges}
-          component={component}
-          branchLike={branchLike}
-          measures={measures}
-          onComponentChange={this.props.onComponentChange}
-          onPageChange={this.setPage}
-        />
-        {canUseBadges && (
-          <InfoDrawerPage
-            displayed={page === ProjectInformationPages.badges}
-            onPageChange={this.setPage}
-          >
-            <ProjectBadges branchLike={branchLike} component={component} />
-          </InfoDrawerPage>
-        )}
-        {canConfigureNotifications && (
-          <InfoDrawerPage
-            displayed={page === ProjectInformationPages.notifications}
-            onPageChange={this.setPage}
-          >
-            <ProjectNotifications component={component} />
-          </InfoDrawerPage>
-        )}
-      </>
-    );
-  }
-}
-
-export default withCurrentUserContext(withMetricsContext(ProjectInformation));
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationPages.ts b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationPages.ts
deleted file mode 100644 (file)
index cafa737..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.
- */
-export enum ProjectInformationPages {
-  main,
-  badges,
-  notifications,
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationRenderer.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationRenderer.tsx
deleted file mode 100644 (file)
index 7383f11..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 PrivacyBadgeContainer from '../../../../../components/common/PrivacyBadgeContainer';
-import { ButtonLink } from '../../../../../components/controls/buttons';
-import ModalButton from '../../../../../components/controls/ModalButton';
-import { translate } from '../../../../../helpers/l10n';
-import { BranchLike } from '../../../../../types/branch-like';
-import { ComponentQualifier } from '../../../../../types/component';
-import { Feature } from '../../../../../types/features';
-import { Component, Measure } from '../../../../../types/types';
-import withAvailableFeatures, {
-  WithAvailableFeaturesProps,
-} from '../../../available-features/withAvailableFeatures';
-import DrawerLink from './DrawerLink';
-import MetaKey from './meta/MetaKey';
-import MetaLinks from './meta/MetaLinks';
-import MetaQualityGate from './meta/MetaQualityGate';
-import MetaQualityProfiles from './meta/MetaQualityProfiles';
-import MetaSize from './meta/MetaSize';
-import MetaTags from './meta/MetaTags';
-import { ProjectInformationPages } from './ProjectInformationPages';
-import RegulatoryReportModal from './projectRegulatoryReport/RegulatoryReportModal';
-
-export interface ProjectInformationRendererProps extends WithAvailableFeaturesProps {
-  canConfigureNotifications: boolean;
-  canUseBadges: boolean;
-  component: Component;
-  branchLike?: BranchLike;
-  measures?: Measure[];
-  onComponentChange: (changes: {}) => void;
-  onPageChange: (page: ProjectInformationPages) => void;
-}
-
-export function ProjectInformationRenderer(props: ProjectInformationRendererProps) {
-  const { canConfigureNotifications, canUseBadges, component, measures = [], branchLike } = props;
-
-  const heading = React.useRef<HTMLHeadingElement>(null);
-  const isApp = component.qualifier === ComponentQualifier.Application;
-
-  React.useEffect(() => {
-    if (heading.current) {
-      // a11y: provide focus to the heading when the Project Information is opened.
-      heading.current.focus();
-    }
-  }, [heading]);
-
-  const regulatoryReportFeatureEnabled = props.hasFeature(Feature.RegulatoryReport);
-
-  return (
-    <>
-      <div>
-        <h2 className="big-padded bordered-bottom" tabIndex={-1} ref={heading}>
-          {translate(isApp ? 'application' : 'project', 'info.title')}
-        </h2>
-      </div>
-
-      <div className="overflow-y-auto">
-        <div className="big-padded bordered-bottom">
-          <div className="display-flex-center">
-            <h3 className="spacer-right">{translate('project.info.description')}</h3>
-            {component.visibility && (
-              <PrivacyBadgeContainer
-                qualifier={component.qualifier}
-                visibility={component.visibility}
-              />
-            )}
-          </div>
-
-          {component.description && (
-            <p className="it__project-description">{component.description}</p>
-          )}
-
-          <MetaTags component={component} onComponentChange={props.onComponentChange} />
-        </div>
-
-        <div className="big-padded bordered-bottom it__project-loc-value">
-          <MetaSize component={component} measures={measures} />
-        </div>
-
-        {!isApp &&
-          (component.qualityGate ||
-            (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
-            <div className="big-padded bordered-bottom">
-              {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
-
-              {component.qualityProfiles && component.qualityProfiles.length > 0 && (
-                <MetaQualityProfiles
-                  headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
-                  profiles={component.qualityProfiles}
-                />
-              )}
-            </div>
-          )}
-
-        {!isApp && <MetaLinks component={component} />}
-
-        <div className="big-padded bordered-bottom">
-          <MetaKey componentKey={component.key} qualifier={component.qualifier} />
-        </div>
-
-        <ul>
-          {canUseBadges && (
-            <li>
-              <DrawerLink
-                label={translate('overview.badges.get_badge', component.qualifier)}
-                onPageChange={props.onPageChange}
-                to={ProjectInformationPages.badges}
-              />
-            </li>
-          )}
-          {canConfigureNotifications && (
-            <li>
-              <DrawerLink
-                label={translate('project.info.to_notifications')}
-                onPageChange={props.onPageChange}
-                to={ProjectInformationPages.notifications}
-              />
-            </li>
-          )}
-          {component.qualifier === ComponentQualifier.Project && regulatoryReportFeatureEnabled && (
-            <li className="big-padded bordered-bottom">
-              <ModalButton
-                modal={({ onClose }) => (
-                  <RegulatoryReportModal
-                    component={component}
-                    branchLike={branchLike}
-                    onClose={onClose}
-                  />
-                )}
-              >
-                {({ onClick }) => (
-                  <ButtonLink onClick={onClick}>{translate('regulatory_report.page')}</ButtonLink>
-                )}
-              </ModalButton>
-            </li>
-          )}
-        </ul>
-      </div>
-    </>
-  );
-}
-
-export default withAvailableFeatures(React.memo(ProjectInformationRenderer));
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeButton.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeButton.tsx
deleted file mode 100644 (file)
index de00c7d..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 * as React from 'react';
-import { Button } from '../../../../../../components/controls/buttons';
-import { translate } from '../../../../../../helpers/l10n';
-import { BadgeType } from './utils';
-
-interface Props {
-  onClick: (type: BadgeType) => void;
-  selected: boolean;
-  type: BadgeType;
-  url: string;
-}
-
-export default class BadgeButton extends React.PureComponent<Props> {
-  handleClick = () => {
-    this.props.onClick(this.props.type);
-  };
-
-  render() {
-    const { selected, type, url } = this.props;
-    const width = type !== BadgeType.measure ? '128px' : undefined;
-    return (
-      <Button className={classNames('badge-button', { selected })} onClick={this.handleClick}>
-        <img alt={translate('overview.badges', type, 'alt')} src={url} width={width} />
-      </Button>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeParams.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/BadgeParams.tsx
deleted file mode 100644 (file)
index 90d3411..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 * as React from 'react';
-import { fetchWebApi } from '../../../../../../api/web-api';
-import Select from '../../../../../../components/controls/Select';
-import { getLocalizedMetricName, translate } from '../../../../../../helpers/l10n';
-import { Dict, Metric } from '../../../../../../types/types';
-import withMetricsContext from '../../../../metrics/withMetricsContext';
-import { BadgeFormats, BadgeOptions, BadgeType } from './utils';
-
-interface Props {
-  className?: string;
-  metrics: Dict<Metric>;
-  options: BadgeOptions;
-  type: BadgeType;
-  updateOptions: (options: Partial<BadgeOptions>) => void;
-}
-
-interface State {
-  badgeMetrics: string[];
-}
-
-export class BadgeParams extends React.PureComponent<Props> {
-  mounted = false;
-
-  state: State = { badgeMetrics: [] };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchBadgeMetrics();
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchBadgeMetrics() {
-    fetchWebApi(false).then(
-      (webservices) => {
-        if (this.mounted) {
-          const domain = webservices.find((d) => d.path === 'api/project_badges');
-          const ws = domain && domain.actions.find((w) => w.key === 'measure');
-          const param = ws && ws.params && ws.params.find((p) => p.key === 'metric');
-          if (param && param.possibleValues) {
-            this.setState({ badgeMetrics: param.possibleValues });
-          }
-        }
-      },
-      () => {}
-    );
-  }
-
-  getColorOptions = () => {
-    return ['white', 'black', 'orange'].map((color) => ({
-      label: translate('overview.badges.options.colors', color),
-      value: color,
-    }));
-  };
-
-  getFormatOptions = () => {
-    return ['md', 'url'].map((format) => ({
-      label: translate('overview.badges.options.formats', format),
-      value: format as BadgeFormats,
-    }));
-  };
-
-  getMetricOptions = () => {
-    return this.state.badgeMetrics.map((key) => {
-      const metric = this.props.metrics[key];
-      return {
-        value: key,
-        label: metric ? getLocalizedMetricName(metric) : key,
-      };
-    });
-  };
-
-  handleFormatChange = ({ value }: { value: BadgeFormats }) => {
-    this.props.updateOptions({ format: value });
-  };
-
-  handleMetricChange = ({ value }: { value: string }) => {
-    this.props.updateOptions({ metric: value });
-  };
-
-  renderBadgeType = (type: BadgeType, options: BadgeOptions) => {
-    if (type === BadgeType.measure) {
-      const metricOptions = this.getMetricOptions();
-      return (
-        <>
-          <label className="spacer-right" htmlFor="badge-metric">
-            {translate('overview.badges.metric')}:
-          </label>
-          <Select
-            className="input-medium it__metric-badge-select"
-            inputId="badge-metric"
-            isSearchable={false}
-            onChange={this.handleMetricChange}
-            options={metricOptions}
-            value={metricOptions.find((o) => o.value === options.metric)}
-          />
-        </>
-      );
-    }
-    return null;
-  };
-
-  render() {
-    const { className, options, type } = this.props;
-    const formatOptions = this.getFormatOptions();
-    return (
-      <div className={className}>
-        {this.renderBadgeType(type, options)}
-
-        <label
-          className={classNames('spacer-right', {
-            'spacer-top': type !== BadgeType.qualityGate,
-          })}
-          htmlFor="badge-format"
-        >
-          {translate('format')}:
-        </label>
-        <Select
-          className="input-medium"
-          inputId="badge-format"
-          isSearchable={false}
-          onChange={this.handleFormatChange}
-          options={formatOptions}
-          value={formatOptions.find((o) => o.value === options.format)}
-          defaultValue={formatOptions.find((o) => o.value === 'md')}
-        />
-      </div>
-    );
-  }
-}
-
-export default withMetricsContext(BadgeParams);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx
deleted file mode 100644 (file)
index ae60a29..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 {
-  getProjectBadgesToken,
-  renewProjectBadgesToken,
-} from '../../../../../../api/project-badges';
-import CodeSnippet from '../../../../../../components/common/CodeSnippet';
-import { Button } from '../../../../../../components/controls/buttons';
-import { Alert } from '../../../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../../../components/ui/DeferredSpinner';
-import { getBranchLikeQuery } from '../../../../../../helpers/branch-like';
-import { translate } from '../../../../../../helpers/l10n';
-import { BranchLike } from '../../../../../../types/branch-like';
-import { MetricKey } from '../../../../../../types/metrics';
-import { Component } from '../../../../../../types/types';
-import BadgeButton from './BadgeButton';
-import BadgeParams from './BadgeParams';
-import './styles.css';
-import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';
-
-interface Props {
-  branchLike?: BranchLike;
-  component: Component;
-}
-
-interface State {
-  isRenewing: boolean;
-  token: string;
-  selectedType: BadgeType;
-  badgeOptions: BadgeOptions;
-}
-
-export default class ProjectBadges extends React.PureComponent<Props, State> {
-  mounted = false;
-  headingNodeRef = React.createRef<HTMLHeadingElement>();
-  state: State = {
-    isRenewing: false,
-    token: '',
-    selectedType: BadgeType.measure,
-    badgeOptions: { metric: MetricKey.alert_status },
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchToken();
-    if (this.headingNodeRef.current) {
-      this.headingNodeRef.current.focus();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  async fetchToken() {
-    const {
-      component: { key },
-    } = this.props;
-    const token = await getProjectBadgesToken(key).catch(() => '');
-    if (this.mounted) {
-      this.setState({ token });
-    }
-  }
-
-  handleSelectBadge = (selectedType: BadgeType) => {
-    this.setState({ selectedType });
-  };
-
-  handleUpdateOptions = (options: Partial<BadgeOptions>) => {
-    this.setState((state) => ({
-      badgeOptions: { ...state.badgeOptions, ...options },
-    }));
-  };
-
-  handleRenew = async () => {
-    const {
-      component: { key },
-    } = this.props;
-
-    this.setState({ isRenewing: true });
-    await renewProjectBadgesToken(key).catch(() => {});
-    await this.fetchToken();
-    if (this.mounted) {
-      this.setState({ isRenewing: false });
-    }
-  };
-
-  render() {
-    const {
-      branchLike,
-      component: { key: project, qualifier, configuration },
-    } = this.props;
-    const { isRenewing, selectedType, badgeOptions, token } = this.state;
-    const fullBadgeOptions = {
-      project,
-      ...badgeOptions,
-      ...getBranchLikeQuery(branchLike),
-    };
-    const canRenew = configuration?.showSettings;
-
-    return (
-      <div className="display-flex-column">
-        <h3 tabIndex={-1} ref={this.headingNodeRef}>
-          {translate('overview.badges.get_badge', qualifier)}
-        </h3>
-        <p className="big-spacer-bottom">{translate('overview.badges.description', qualifier)}</p>
-        <BadgeButton
-          onClick={this.handleSelectBadge}
-          selected={BadgeType.measure === selectedType}
-          type={BadgeType.measure}
-          url={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
-        />
-        <p className="huge-spacer-bottom spacer-top">
-          {translate('overview.badges', BadgeType.measure, 'description', qualifier)}
-        </p>
-        <BadgeButton
-          onClick={this.handleSelectBadge}
-          selected={BadgeType.qualityGate === selectedType}
-          type={BadgeType.qualityGate}
-          url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
-        />
-        <p className="huge-spacer-bottom spacer-top">
-          {translate('overview.badges', BadgeType.qualityGate, 'description', qualifier)}
-        </p>
-        <BadgeParams
-          className="big-spacer-bottom display-flex-column"
-          options={badgeOptions}
-          type={selectedType}
-          updateOptions={this.handleUpdateOptions}
-        />
-        {isRenewing ? (
-          <div className="spacer-top spacer-bottom display-flex-row display-flex-justify-center">
-            <DeferredSpinner className="spacer-top spacer-bottom" loading={isRenewing} />
-          </div>
-        ) : (
-          <CodeSnippet isOneLine snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} />
-        )}
-
-        <Alert variant="warning">
-          <p>
-            {translate('overview.badges.leak_warning')}{' '}
-            {canRenew && translate('overview.badges.renew.description')}
-          </p>
-          {canRenew && (
-            <Button
-              disabled={isRenewing}
-              className="spacer-top it__project-info-renew-badge"
-              onClick={this.handleRenew}
-            >
-              {translate('overview.badges.renew')}
-            </Button>
-          )}
-        </Alert>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx
deleted file mode 100644 (file)
index 0ce8130..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 selectEvent from 'react-select-event';
-import { getProjectBadgesToken } from '../../../../../../../api/project-badges';
-import { mockBranch } from '../../../../../../../helpers/mocks/branch-like';
-import { mockComponent } from '../../../../../../../helpers/mocks/component';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { Location } from '../../../../../../../helpers/urls';
-import { ComponentQualifier } from '../../../../../../../types/component';
-import { MetricKey } from '../../../../../../../types/metrics';
-import ProjectBadges from '../ProjectBadges';
-import { BadgeType } from '../utils';
-
-jest.mock('../../../../../../../helpers/urls', () => ({
-  getHostUrl: () => 'host',
-  getPathUrlAsString: (l: Location) => l.pathname,
-  getProjectUrl: () => ({ pathname: '/dashboard' } as Location),
-}));
-
-jest.mock('../../../../../../../api/project-badges', () => ({
-  getProjectBadgesToken: jest.fn().mockResolvedValue('foo'),
-  renewProjectBadgesToken: jest.fn().mockResolvedValue({}),
-}));
-
-jest.mock('../../../../../../../api/web-api', () => ({
-  fetchWebApi: () =>
-    Promise.resolve([
-      {
-        path: 'api/project_badges',
-        actions: [
-          {
-            key: 'measure',
-            // eslint-disable-next-line local-rules/use-metrickey-enum
-            params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage'] }],
-          },
-        ],
-      },
-    ]),
-}));
-
-it('should renew token', async () => {
-  const user = userEvent.setup();
-  jest.mocked(getProjectBadgesToken).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar');
-  renderProjectBadges({
-    component: mockComponent({ configuration: { showSettings: true } }),
-  });
-
-  expect(
-    await screen.findByText(`overview.badges.get_badge.${ComponentQualifier.Project}`)
-  ).toHaveFocus();
-
-  expect(screen.getByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)).toHaveAttribute(
-    'src',
-    'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo'
-  );
-
-  expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
-    'src',
-    'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo'
-  );
-
-  await user.click(screen.getByText('overview.badges.renew'));
-
-  expect(
-    await screen.findByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)
-  ).toHaveAttribute(
-    'src',
-    'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar'
-  );
-
-  expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
-    'src',
-    'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=bar'
-  );
-});
-
-it('should update params', async () => {
-  renderProjectBadges({
-    component: mockComponent({ configuration: { showSettings: true } }),
-  });
-
-  expect(
-    await screen.findByText(
-      '[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo)](/dashboard)'
-    )
-  ).toBeInTheDocument();
-
-  await selectEvent.select(screen.getByLabelText('format:'), [
-    'overview.badges.options.formats.url',
-  ]);
-
-  expect(
-    screen.getByText(
-      'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo'
-    )
-  ).toBeInTheDocument();
-
-  await selectEvent.select(screen.getByLabelText('overview.badges.metric:'), MetricKey.coverage);
-
-  expect(
-    screen.getByText(
-      `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo`
-    )
-  ).toBeInTheDocument();
-});
-
-function renderProjectBadges(props: Partial<ProjectBadges['props']> = {}) {
-  return renderComponent(
-    <ProjectBadges
-      branchLike={mockBranch()}
-      component={mockComponent({ key: 'foo', qualifier: ComponentQualifier.Project })}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/utils-test.ts
deleted file mode 100644 (file)
index c29891a..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { Location } from '../../../../../../../components/hoc/withRouter';
-import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from '../utils';
-
-jest.mock('../../../../../../../helpers/urls', () => ({
-  ...jest.requireActual('../../../../../../../helpers/urls'),
-  getHostUrl: () => 'host',
-  getPathUrlAsString: (o: Location) => `host${o.pathname}${o.search}`,
-}));
-
-const options: BadgeOptions = {
-  branch: 'master',
-  metric: 'alert_status',
-  project: 'foo',
-};
-
-describe('#getBadgeUrl', () => {
-  it('should generate correct quality gate badge links', () => {
-    expect(getBadgeUrl(BadgeType.qualityGate, options, 'foo')).toBe(
-      'host/api/project_badges/quality_gate?branch=master&project=foo&token=foo'
-    );
-  });
-
-  it('should generate correct measures badge links', () => {
-    expect(getBadgeUrl(BadgeType.measure, options, 'foo')).toBe(
-      'host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo'
-    );
-  });
-
-  it('should ignore undefined parameters', () => {
-    expect(getBadgeUrl(BadgeType.measure, { metric: 'alert_status' }, 'foo')).toBe(
-      'host/api/project_badges/measure?metric=alert_status&token=foo'
-    );
-  });
-
-  it('should force metric parameters', () => {
-    expect(getBadgeUrl(BadgeType.measure, {}, 'foo')).toBe(
-      'host/api/project_badges/measure?metric=alert_status&token=foo'
-    );
-  });
-});
-
-describe('#getBadgeSnippet', () => {
-  it('should generate a correct markdown image', () => {
-    expect(getBadgeSnippet(BadgeType.measure, { ...options, format: 'md' }, 'foo')).toBe(
-      '[![alert_status](host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo)](host/dashboard?id=foo&branch=master)'
-    );
-  });
-});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/styles.css b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/styles.css
deleted file mode 100644 (file)
index 8dca6fd..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.
- */
-.badges-list {
-  display: flex;
-  justify-content: space-around;
-  justify-content: space-evenly;
-  flex-wrap: nowrap;
-}
-
-.button.badge-button {
-  display: flex;
-  justify-content: center;
-  padding: var(--gridSize);
-  min-width: 146px;
-  height: 116px;
-  background-color: var(--barBackgroundColor);
-  border: solid 1px var(--barBorderColor);
-  border-radius: 3px;
-  transition: all 0.3s ease;
-}
-
-.button.badge-button:hover,
-.button.badge-button:focus,
-.button.badge-button:active {
-  background-color: var(--barBackgroundColor);
-  border-color: var(--blue);
-}
-
-.button.badge-button.selected {
-  background-color: var(--lightBlue);
-  border-color: var(--darkBlue);
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/utils.ts
deleted file mode 100644 (file)
index 15f2c88..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { getLocalizedMetricName } from '../../../../../../helpers/l10n';
-import { omitNil } from '../../../../../../helpers/request';
-import { getHostUrl, getPathUrlAsString, getProjectUrl } from '../../../../../../helpers/urls';
-
-export type BadgeColors = 'white' | 'black' | 'orange';
-export type BadgeFormats = 'md' | 'url';
-
-export interface BadgeOptions {
-  branch?: string;
-  format?: BadgeFormats;
-  project?: string;
-  metric?: string;
-  pullRequest?: string;
-}
-
-export enum BadgeType {
-  measure = 'measure',
-  qualityGate = 'quality_gate',
-}
-
-export function getBadgeSnippet(type: BadgeType, options: BadgeOptions, token: string) {
-  const url = getBadgeUrl(type, options, token);
-  const { branch, format = 'md', metric = 'alert_status', project } = options;
-
-  if (format === 'url') {
-    return url;
-  }
-
-  let label;
-  let projectUrl;
-
-  switch (type) {
-    case BadgeType.measure:
-      label = getLocalizedMetricName({ key: metric });
-      break;
-    case BadgeType.qualityGate:
-    default:
-      label = 'Quality gate';
-      break;
-  }
-
-  if (project) {
-    projectUrl = getPathUrlAsString(getProjectUrl(project, branch), false);
-  }
-
-  const mdImage = `![${label}](${url})`;
-  return projectUrl ? `[${mdImage}](${projectUrl})` : mdImage;
-}
-
-export function getBadgeUrl(
-  type: BadgeType,
-  { branch, project, metric = 'alert_status', pullRequest }: BadgeOptions,
-  token: string
-) {
-  switch (type) {
-    case BadgeType.qualityGate:
-      return `${getHostUrl()}/api/project_badges/quality_gate?${new URLSearchParams(
-        omitNil({ branch, project, pullRequest, token })
-      ).toString()}`;
-    case BadgeType.measure:
-    default:
-      return `${getHostUrl()}/api/project_badges/measure?${new URLSearchParams(
-        omitNil({ branch, project, metric, pullRequest, token })
-      ).toString()}`;
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaKey.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaKey.tsx
deleted file mode 100644 (file)
index 432b713..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { ClipboardButton } from '../../../../../../components/controls/clipboard';
-import { translate } from '../../../../../../helpers/l10n';
-
-export interface MetaKeyProps {
-  componentKey: string;
-  qualifier: string;
-}
-
-export default function MetaKey({ componentKey, qualifier }: MetaKeyProps) {
-  return (
-    <>
-      <h3 id="project-key">{translate('overview.project_key', qualifier)}</h3>
-      <div className="display-flex-center">
-        <input
-          className="overview-key"
-          aria-labelledby="project-key"
-          readOnly
-          type="text"
-          value={componentKey}
-        />
-        <ClipboardButton
-          aria-label={translate('overview.project_key.click_to_copy')}
-          className="little-spacer-left"
-          copyValue={componentKey}
-        />
-      </div>
-    </>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLink.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLink.tsx
deleted file mode 100644 (file)
index 03d41ac..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { ClearButton } from '../../../../../../components/controls/buttons';
-import ProjectLinkIcon from '../../../../../../components/icons/ProjectLinkIcon';
-import { getLinkName } from '../../../../../../helpers/projectLinks';
-import { ProjectLink } from '../../../../../../types/types';
-import isValidUri from '../../../../../utils/isValidUri';
-
-interface Props {
-  iconOnly?: boolean;
-  link: ProjectLink;
-}
-
-interface State {
-  expanded: boolean;
-}
-
-export default class MetaLink extends React.PureComponent<Props, State> {
-  state = { expanded: false };
-
-  handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    this.setState(({ expanded }) => ({ expanded: !expanded }));
-  };
-
-  handleCollapse = () => {
-    this.setState({ expanded: false });
-  };
-
-  handleSelect = (event: React.MouseEvent<HTMLInputElement>) => {
-    event.currentTarget.select();
-  };
-
-  render() {
-    const { iconOnly, link } = this.props;
-    const linkTitle = getLinkName(link);
-    const isValid = isValidUri(link.url);
-    return (
-      <li>
-        <a
-          className="link-no-underline"
-          href={isValid ? link.url : undefined}
-          onClick={isValid ? undefined : this.handleClick}
-          rel="nofollow noreferrer noopener"
-          target="_blank"
-          title={linkTitle}
-        >
-          <ProjectLinkIcon className="little-spacer-right" type={link.type} />
-          {!iconOnly && linkTitle}
-        </a>
-        {this.state.expanded && (
-          <div className="little-spacer-top display-flex-center">
-            <input
-              className="overview-key width-80"
-              onClick={this.handleSelect}
-              readOnly
-              type="text"
-              value={link.url}
-            />
-            <ClearButton className="little-spacer-left" onClick={this.handleCollapse} />
-          </div>
-        )}
-      </li>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLinks.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaLinks.tsx
deleted file mode 100644 (file)
index 8976d94..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { getProjectLinks } from '../../../../../../api/projectLinks';
-import { translate } from '../../../../../../helpers/l10n';
-import { orderLinks } from '../../../../../../helpers/projectLinks';
-import { LightComponent, ProjectLink } from '../../../../../../types/types';
-import MetaLink from './MetaLink';
-
-interface Props {
-  component: LightComponent;
-}
-
-interface State {
-  links?: ProjectLink[];
-}
-
-export default class MetaLinks extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = {};
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadLinks();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.component.key !== this.props.component.key) {
-      this.loadLinks();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  loadLinks = () =>
-    getProjectLinks(this.props.component.key).then(
-      (links) => {
-        if (this.mounted) {
-          this.setState({ links });
-        }
-      },
-      () => {}
-    );
-
-  render() {
-    const { links } = this.state;
-
-    if (!links || links.length === 0) {
-      return null;
-    }
-
-    const orderedLinks = orderLinks(links);
-
-    return (
-      <div className="big-padded bordered-bottom">
-        <h3>{translate('overview.external_links')}</h3>
-        <ul className="project-info-list">
-          {orderedLinks.map((link) => (
-            <MetaLink key={link.id} link={link} />
-          ))}
-        </ul>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityGate.tsx
deleted file mode 100644 (file)
index a6f4637..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 } from '../../../../../../helpers/l10n';
-import { getQualityGateUrl } from '../../../../../../helpers/urls';
-
-interface Props {
-  qualityGate: { isDefault?: boolean; name: string };
-}
-
-export default function MetaQualityGate({ qualityGate }: Props) {
-  return (
-    <>
-      <h3>{translate('project.info.quality_gate')}</h3>
-
-      <ul className="project-info-list">
-        <li>
-          {qualityGate.isDefault && (
-            <span className="note spacer-right">({translate('default')})</span>
-          )}
-          <Link to={getQualityGateUrl(qualityGate.name)}>{qualityGate.name}</Link>
-        </li>
-      </ul>
-    </>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaQualityProfiles.tsx
deleted file mode 100644 (file)
index 514218f..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { searchRules } from '../../../../../../api/rules';
-import Link from '../../../../../../components/common/Link';
-import Tooltip from '../../../../../../components/controls/Tooltip';
-import { translate, translateWithParameters } from '../../../../../../helpers/l10n';
-import { getQualityProfileUrl } from '../../../../../../helpers/urls';
-import { Languages } from '../../../../../../types/languages';
-import { ComponentQualityProfile, Dict } from '../../../../../../types/types';
-import withLanguagesContext from '../../../../languages/withLanguagesContext';
-
-interface Props {
-  headerClassName?: string;
-  languages: Languages;
-  profiles: ComponentQualityProfile[];
-}
-
-interface State {
-  deprecatedByKey: Dict<number>;
-}
-
-export class MetaQualityProfiles extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { deprecatedByKey: {} };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadDeprecatedRules();
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  loadDeprecatedRules() {
-    const existingProfiles = this.props.profiles.filter((p) => !p.deleted);
-    const requests = existingProfiles.map((profile) =>
-      this.loadDeprecatedRulesForProfile(profile.key)
-    );
-    Promise.all(requests).then(
-      (responses) => {
-        if (this.mounted) {
-          const deprecatedByKey: Dict<number> = {};
-          responses.forEach((count, i) => {
-            const profileKey = existingProfiles[i].key;
-            deprecatedByKey[profileKey] = count;
-          });
-          this.setState({ deprecatedByKey });
-        }
-      },
-      () => {}
-    );
-  }
-
-  loadDeprecatedRulesForProfile(profileKey: string) {
-    const data = {
-      activation: 'true',
-      ps: 1,
-      qprofile: profileKey,
-      statuses: 'DEPRECATED',
-    };
-    return searchRules(data).then((r) => r.paging.total);
-  }
-
-  getDeprecatedRulesCount(profile: { key: string }) {
-    const count = this.state.deprecatedByKey[profile.key];
-    return count || 0;
-  }
-
-  renderProfile(profile: ComponentQualityProfile) {
-    const languageFromStore = this.props.languages[profile.language];
-    const languageName = languageFromStore ? languageFromStore.name : profile.language;
-
-    const inner = (
-      <div className="text-ellipsis">
-        <span className="spacer-right">({languageName})</span>
-        {profile.deleted ? (
-          profile.name
-        ) : (
-          <Link to={getQualityProfileUrl(profile.name, profile.language)}>
-            <span
-              aria-label={translateWithParameters(
-                'overview.link_to_x_profile_y',
-                languageName,
-                profile.name
-              )}
-            >
-              {profile.name}
-            </span>
-          </Link>
-        )}
-      </div>
-    );
-
-    if (profile.deleted) {
-      const tooltip = translateWithParameters('overview.deleted_profile', profile.name);
-      return (
-        <Tooltip key={profile.key} overlay={tooltip}>
-          <li className="project-info-deleted-profile">{inner}</li>
-        </Tooltip>
-      );
-    }
-
-    const count = this.getDeprecatedRulesCount(profile);
-
-    if (count > 0) {
-      const tooltip = translateWithParameters('overview.deprecated_profile', count);
-      return (
-        <Tooltip key={profile.key} overlay={tooltip}>
-          <li className="project-info-deprecated-rules">{inner}</li>
-        </Tooltip>
-      );
-    }
-
-    return <li key={profile.key}>{inner}</li>;
-  }
-
-  render() {
-    const { headerClassName, profiles } = this.props;
-
-    return (
-      <>
-        <h3 className={headerClassName}>{translate('overview.quality_profiles')}</h3>
-
-        <ul className="project-info-list">
-          {profiles.map((profile) => this.renderProfile(profile))}
-        </ul>
-      </>
-    );
-  }
-}
-
-export default withLanguagesContext(MetaQualityProfiles);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaSize.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaSize.tsx
deleted file mode 100644 (file)
index 66d0d37..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 DrilldownLink from '../../../../../../components/shared/DrilldownLink';
-import SizeRating from '../../../../../../components/ui/SizeRating';
-import { translate, translateWithParameters } from '../../../../../../helpers/l10n';
-import { formatMeasure, localizeMetric } from '../../../../../../helpers/measures';
-import { ComponentQualifier } from '../../../../../../types/component';
-import { MetricKey } from '../../../../../../types/metrics';
-import { Component, Measure } from '../../../../../../types/types';
-
-export interface MetaSizeProps {
-  component: Component;
-  measures: Measure[];
-}
-
-export default function MetaSize({ component, measures }: MetaSizeProps) {
-  const isApp = component.qualifier === ComponentQualifier.Application;
-  const ncloc = measures.find((measure) => measure.metric === MetricKey.ncloc);
-  const projects = isApp
-    ? measures.find((measure) => measure.metric === MetricKey.projects)
-    : undefined;
-
-  return (
-    <>
-      <div className="display-flex-row display-inline-flex-baseline">
-        <h3>{localizeMetric(MetricKey.ncloc)}</h3>
-        <span className="spacer-left small">({translate('project.info.main_branch')})</span>
-      </div>
-      <div className="display-flex-center">
-        {ncloc && ncloc.value ? (
-          <>
-            <DrilldownLink className="huge" component={component.key} metric={MetricKey.ncloc}>
-              <span
-                aria-label={translateWithParameters(
-                  'project.info.see_more_info_on_x_locs',
-                  ncloc.value
-                )}
-              >
-                {formatMeasure(ncloc.value, 'SHORT_INT')}
-              </span>
-            </DrilldownLink>
-
-            <span className="spacer-left">
-              <SizeRating value={Number(ncloc.value)} />
-            </span>
-          </>
-        ) : (
-          <span>0</span>
-        )}
-
-        {isApp && (
-          <span className="huge-spacer-left display-inline-flex-center">
-            {projects ? (
-              <DrilldownLink component={component.key} metric={MetricKey.projects}>
-                <span className="big">{formatMeasure(projects.value, 'SHORT_INT')}</span>
-              </DrilldownLink>
-            ) : (
-              <span className="big">0</span>
-            )}
-            <span className="little-spacer-left text-muted">
-              {translate('metric.projects.name')}
-            </span>
-          </span>
-        )}
-      </div>
-    </>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTags.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTags.tsx
deleted file mode 100644 (file)
index f384bb5..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { setApplicationTags, setProjectTags } from '../../../../../../api/components';
-import { ButtonLink } from '../../../../../../components/controls/buttons';
-import Dropdown from '../../../../../../components/controls/Dropdown';
-import TagsList from '../../../../../../components/tags/TagsList';
-import { PopupPlacement } from '../../../../../../components/ui/popups';
-import { translate } from '../../../../../../helpers/l10n';
-import { ComponentQualifier } from '../../../../../../types/component';
-import { Component } from '../../../../../../types/types';
-import MetaTagsSelector from './MetaTagsSelector';
-
-interface Props {
-  component: Component;
-  onComponentChange: (changes: {}) => void;
-}
-
-export default class MetaTags extends React.PureComponent<Props> {
-  canUpdateTags = () => {
-    const { configuration } = this.props.component;
-    return configuration && configuration.showSettings;
-  };
-
-  setTags = (values: string[]) => {
-    const { component } = this.props;
-
-    if (component.qualifier === ComponentQualifier.Application) {
-      return setApplicationTags({
-        application: component.key,
-        tags: values.join(','),
-      });
-    }
-
-    return setProjectTags({
-      project: component.key,
-      tags: values.join(','),
-    });
-  };
-
-  handleSetProjectTags = (values: string[]) => {
-    this.setTags(values).then(
-      () => this.props.onComponentChange({ tags: values }),
-      () => {}
-    );
-  };
-
-  render() {
-    const tags = this.props.component.tags || [];
-
-    return this.canUpdateTags() ? (
-      <div className="big-spacer-top project-info-tags">
-        <Dropdown
-          closeOnClick={false}
-          closeOnClickOutside
-          overlay={
-            <MetaTagsSelector selectedTags={tags} setProjectTags={this.handleSetProjectTags} />
-          }
-          overlayPlacement={PopupPlacement.BottomLeft}
-        >
-          <ButtonLink stopPropagation>
-            <TagsList allowUpdate tags={tags.length ? tags : [translate('no_tags')]} />
-          </ButtonLink>
-        </Dropdown>
-      </div>
-    ) : (
-      <div className="big-spacer-top project-info-tags">
-        <TagsList
-          allowUpdate={false}
-          className="note"
-          tags={tags.length ? tags : [translate('no_tags')]}
-        />
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTagsSelector.tsx
deleted file mode 100644 (file)
index b7bc47f..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { difference, without } from 'lodash';
-import * as React from 'react';
-import { searchProjectTags } from '../../../../../../api/components';
-import TagsSelector from '../../../../../../components/tags/TagsSelector';
-
-interface Props {
-  selectedTags: string[];
-  setProjectTags: (tags: string[]) => void;
-}
-
-interface State {
-  searchResult: string[];
-}
-
-const LIST_SIZE = 10;
-
-export default class MetaTagsSelector extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { searchResult: [] };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  onSearch = (query: string) => {
-    return searchProjectTags({
-      q: query,
-      ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
-    }).then(
-      ({ tags }) => {
-        if (this.mounted) {
-          this.setState({ searchResult: tags });
-        }
-      },
-      () => {}
-    );
-  };
-
-  onSelect = (tag: string) => {
-    this.props.setProjectTags([...this.props.selectedTags, tag]);
-  };
-
-  onUnselect = (tag: string) => {
-    this.props.setProjectTags(without(this.props.selectedTags, tag));
-  };
-
-  render() {
-    const availableTags = difference(this.state.searchResult, this.props.selectedTags);
-    return (
-      <TagsSelector
-        listSize={LIST_SIZE}
-        onSearch={this.onSearch}
-        onSelect={this.onSelect}
-        onUnselect={this.onUnselect}
-        selectedTags={this.props.selectedTags}
-        tags={availableTags}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaKey-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaKey-test.tsx
deleted file mode 100644 (file)
index 4f55e67..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { ComponentQualifier } from '../../../../../../../types/component';
-import MetaKey, { MetaKeyProps } from '../MetaKey';
-
-it('should render correctly', () => {
-  renderMetaKey();
-  expect(
-    screen.getByLabelText(`overview.project_key.${ComponentQualifier.Project}`)
-  ).toBeInTheDocument();
-  expect(
-    screen.getByRole('button', { name: 'overview.project_key.click_to_copy' })
-  ).toBeInTheDocument();
-});
-
-function renderMetaKey(props: Partial<MetaKeyProps> = {}) {
-  return renderComponent(
-    <MetaKey componentKey="foo" qualifier={ComponentQualifier.Project} {...props} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx
deleted file mode 100644 (file)
index 5d6b2e7..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { searchRules } from '../../../../../../../api/rules';
-import {
-  mockLanguage,
-  mockPaging,
-  mockQualityProfile,
-} from '../../../../../../../helpers/testMocks';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { SearchRulesResponse } from '../../../../../../../types/coding-rules';
-import { Dict } from '../../../../../../../types/types';
-import { MetaQualityProfiles } from '../MetaQualityProfiles';
-
-jest.mock('../../../../../../../api/rules', () => {
-  return {
-    searchRules: jest.fn().mockResolvedValue({
-      total: 10,
-    }),
-  };
-});
-
-it('should render correctly', async () => {
-  const totals: Dict<number> = {
-    js: 0,
-    ts: 10,
-    css: 0,
-  };
-  jest
-    .mocked(searchRules)
-    .mockImplementation(({ qprofile }: { qprofile: string }): Promise<SearchRulesResponse> => {
-      return Promise.resolve({
-        rules: [],
-        paging: mockPaging({
-          total: totals[qprofile],
-        }),
-      });
-    });
-
-  renderMetaQualityprofiles();
-
-  expect(await screen.findByText('overview.deleted_profile.javascript')).toBeInTheDocument();
-  expect(screen.getByText('overview.deprecated_profile.10')).toBeInTheDocument();
-});
-
-function renderMetaQualityprofiles(overrides: Partial<MetaQualityProfiles['props']> = {}) {
-  return renderComponent(
-    <MetaQualityProfiles
-      languages={{ css: mockLanguage() }}
-      profiles={[
-        { ...mockQualityProfile({ key: 'js', name: 'javascript' }), deleted: true },
-        { ...mockQualityProfile({ key: 'ts', name: 'typescript' }), deleted: false },
-        {
-          ...mockQualityProfile({
-            key: 'css',
-            name: 'style',
-            language: 'css',
-            languageName: 'CSS',
-          }),
-          deleted: false,
-        },
-      ]}
-      {...overrides}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaTags-test.tsx
deleted file mode 100644 (file)
index c4c8804..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 {
-  searchProjectTags,
-  setApplicationTags,
-  setProjectTags,
-} from '../../../../../../../api/components';
-import { mockComponent } from '../../../../../../../helpers/mocks/component';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { ComponentQualifier } from '../../../../../../../types/component';
-import MetaTags from '../MetaTags';
-
-jest.mock('../../../../../../../api/components', () => ({
-  setApplicationTags: jest.fn().mockResolvedValue(true),
-  setProjectTags: jest.fn().mockResolvedValue(true),
-  searchProjectTags: jest.fn(),
-}));
-
-beforeEach(() => {
-  jest.clearAllMocks();
-});
-
-it('should render without tags and admin rights', async () => {
-  renderMetaTags();
-
-  expect(await screen.findByText('no_tags')).toBeInTheDocument();
-  expect(screen.queryByRole('button')).not.toBeInTheDocument();
-});
-
-it('should allow to edit tags for a project', async () => {
-  const user = userEvent.setup();
-  jest.mocked(searchProjectTags).mockResolvedValue({ tags: ['best', 'useless'] });
-
-  const onComponentChange = jest.fn();
-  const component = mockComponent({
-    key: 'my-second-project',
-    tags: ['foo', 'bar'],
-    configuration: {
-      showSettings: true,
-    },
-    name: 'MySecondProject',
-  });
-
-  renderMetaTags({ component, onComponentChange });
-
-  expect(await screen.findByText('foo, bar')).toBeInTheDocument();
-  expect(screen.getByRole('button')).toBeInTheDocument();
-
-  await user.click(screen.getByRole('button', { name: 'tags_list_x.foo, bar' }));
-
-  expect(await screen.findByText('best')).toBeInTheDocument();
-
-  await user.click(screen.getByText('best'));
-  expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo', 'bar', 'best'] });
-
-  onComponentChange.mockClear();
-
-  /*
-   * Since we're not actually updating the tags, we're back to having the foo, bar only
-   */
-  await user.click(screen.getByText('bar'));
-  expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo'] });
-
-  expect(setProjectTags).toHaveBeenCalled();
-  expect(setApplicationTags).not.toHaveBeenCalled();
-});
-
-it('should set tags for an app', async () => {
-  const user = userEvent.setup();
-
-  renderMetaTags({
-    component: mockComponent({
-      configuration: {
-        showSettings: true,
-      },
-      qualifier: ComponentQualifier.Application,
-    }),
-  });
-
-  await user.click(screen.getByRole('button', { name: 'tags_list_x.no_tags' }));
-
-  await user.click(screen.getByText('best'));
-
-  expect(setProjectTags).not.toHaveBeenCalled();
-  expect(setApplicationTags).toHaveBeenCalled();
-});
-
-function renderMetaTags(overrides: Partial<MetaTags['props']> = {}) {
-  const component = mockComponent({
-    configuration: {
-      showSettings: false,
-    },
-  });
-
-  return renderComponent(
-    <MetaTags component={component} onComponentChange={jest.fn()} {...overrides} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/ProjectNotifications.tsx
deleted file mode 100644 (file)
index b8d254a..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 NotificationsList from '../../../../../../apps/account/notifications/NotificationsList';
-import {
-  withNotifications,
-  WithNotificationsProps,
-} from '../../../../../../components/hoc/withNotifications';
-import { Alert } from '../../../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../../../components/ui/DeferredSpinner';
-import { translate } from '../../../../../../helpers/l10n';
-import { Component } from '../../../../../../types/types';
-
-interface Props {
-  component: Component;
-}
-
-export function ProjectNotifications(props: WithNotificationsProps & Props) {
-  const { channels, component, loading, notifications, perProjectTypes } = props;
-  const heading = React.useRef<HTMLHeadingElement>(null);
-
-  React.useEffect(() => {
-    if (heading.current) {
-      // a11y: provide focus to the heading when the info drawer page is opened.
-      heading.current.focus();
-    }
-  }, [heading]);
-
-  const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
-    props.addNotification({ project: component.key, channel, type });
-  };
-
-  const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
-    props.removeNotification({
-      project: component.key,
-      channel,
-      type,
-    });
-  };
-
-  const getCheckboxId = (type: string, channel: string) => {
-    return `project-notification-${component.key}-${type}-${channel}`;
-  };
-
-  const projectNotifications = notifications.filter(
-    (n) => n.project && n.project === component.key
-  );
-
-  return (
-    <>
-      <h3 tabIndex={-1} ref={heading}>
-        {translate('project.info.notifications')}
-      </h3>
-
-      <Alert className="spacer-top" variant="info">
-        {translate('notification.dispatcher.information')}
-      </Alert>
-
-      <DeferredSpinner loading={loading}>
-        <table className="data zebra notifications-table">
-          <thead>
-            <tr>
-              <th aria-label={translate('project')} />
-              {channels.map((channel) => (
-                <th className="text-center" key={channel}>
-                  <h4>{translate('notification.channel', channel)}</h4>
-                </th>
-              ))}
-            </tr>
-          </thead>
-
-          <NotificationsList
-            channels={channels}
-            checkboxId={getCheckboxId}
-            notifications={projectNotifications}
-            onAdd={handleAddNotification}
-            onRemove={handleRemoveNotification}
-            project
-            types={perProjectTypes}
-          />
-        </table>
-      </DeferredSpinner>
-    </>
-  );
-}
-
-export default withNotifications(ProjectNotifications);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx
deleted file mode 100644 (file)
index a995020..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { getNotifications } from '../../../../../../../api/notifications';
-import { mockComponent } from '../../../../../../../helpers/mocks/component';
-import { mockNotification } from '../../../../../../../helpers/testMocks';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import {
-  NotificationGlobalType,
-  NotificationProjectType,
-} from '../../../../../../../types/notifications';
-import ProjectNotifications from '../ProjectNotifications';
-
-jest.mock('../../../../../../../api/notifications', () => ({
-  addNotification: jest.fn().mockResolvedValue(undefined),
-  removeNotification: jest.fn().mockResolvedValue(undefined),
-  getNotifications: jest.fn(),
-}));
-
-beforeAll(() => {
-  jest.mocked(getNotifications).mockResolvedValue({
-    channels: ['channel1'],
-    globalTypes: [NotificationGlobalType.MyNewIssues],
-    notifications: [
-      mockNotification({}),
-      mockNotification({ type: NotificationProjectType.NewAlerts }),
-    ],
-    perProjectTypes: [NotificationProjectType.NewAlerts, NotificationProjectType.NewIssues],
-  });
-});
-
-it('should render correctly', async () => {
-  const user = userEvent.setup();
-  renderProjectNotifications();
-
-  expect(await screen.findByText('notification.channel.channel1')).toBeInTheDocument();
-  expect(
-    screen.getByLabelText(
-      'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
-    )
-  ).toBeChecked();
-
-  expect(
-    screen.getByLabelText(
-      'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
-    )
-  ).not.toBeChecked();
-
-  // Toggle New Alerts
-  await user.click(
-    screen.getByLabelText(
-      'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
-    )
-  );
-
-  expect(
-    screen.getByLabelText(
-      'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
-    )
-  ).not.toBeChecked();
-
-  // Toggle New Issues
-  await user.click(
-    screen.getByLabelText(
-      'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
-    )
-  );
-
-  expect(
-    screen.getByLabelText(
-      'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
-    )
-  ).toBeChecked();
-});
-
-function renderProjectNotifications() {
-  return renderComponent(
-    <ProjectNotifications component={mockComponent({ key: 'foo', name: 'Foo' })} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/RegulatoryReport.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/RegulatoryReport.tsx
deleted file mode 100644 (file)
index 62df8b7..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { orderBy } from 'lodash';
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { getBranches } from '../../../../../../api/branches';
-import { getRegulatoryReportUrl } from '../../../../../../api/regulatory-report';
-import DocLink from '../../../../../../components/common/DocLink';
-import Select, { LabelValueSelectOption } from '../../../../../../components/controls/Select';
-import { ButtonLink } from '../../../../../../components/controls/buttons';
-import { Alert } from '../../../../../../components/ui/Alert';
-import {
-  getBranchLikeDisplayName,
-  getBranchLikeKey,
-  isMainBranch,
-} from '../../../../../../helpers/branch-like';
-import { translate } from '../../../../../../helpers/l10n';
-import { BranchLike } from '../../../../../../types/branch-like';
-import { Component } from '../../../../../../types/types';
-
-interface Props {
-  component: Pick<Component, 'key' | 'name'>;
-  branchLike?: BranchLike;
-  onClose: () => void;
-}
-
-interface State {
-  downloadStarted: boolean;
-  selectedBranch: string;
-  branchOptions: LabelValueSelectOption[];
-}
-
-export default class RegulatoryReport extends React.PureComponent<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      downloadStarted: false,
-      selectedBranch: '',
-      branchOptions: [],
-    };
-  }
-
-  componentDidMount() {
-    const { component, branchLike } = this.props;
-    getBranches(component.key)
-      .then((data) => {
-        const availableBranches = data.filter(
-          (br) => br.analysisDate && (isMainBranch(br) || br.excludedFromPurge)
-        );
-        const mainBranch = availableBranches.find(isMainBranch);
-        const otherBranchSorted = orderBy(
-          availableBranches.filter((b) => !isMainBranch(b)),
-          (b) => b.name
-        );
-        const sortedBranch = mainBranch ? [mainBranch, ...otherBranchSorted] : otherBranchSorted;
-        const options = sortedBranch.map((br) => {
-          return {
-            value: getBranchLikeDisplayName(br),
-            label: getBranchLikeDisplayName(br),
-          };
-        });
-
-        let selectedBranch = '';
-        if (
-          branchLike &&
-          availableBranches.find((br) => getBranchLikeKey(br) === getBranchLikeKey(branchLike))
-        ) {
-          selectedBranch = getBranchLikeDisplayName(branchLike);
-        } else if (mainBranch) {
-          selectedBranch = getBranchLikeDisplayName(mainBranch);
-        }
-        this.setState({ selectedBranch, branchOptions: options });
-      })
-      .catch(() => {
-        this.setState({ branchOptions: [] });
-      });
-  }
-
-  onBranchSelect = (newOption: LabelValueSelectOption) => {
-    this.setState({ selectedBranch: newOption.value, downloadStarted: false });
-  };
-
-  render() {
-    const { component, onClose } = this.props;
-    const { downloadStarted, selectedBranch, branchOptions } = this.state;
-    const isDownloadButtonDisabled = downloadStarted || !selectedBranch;
-
-    return (
-      <>
-        <div className="modal-head">
-          <h2>{translate('regulatory_report.page')}</h2>
-        </div>
-        <div className="modal-body">
-          <p>{translate('regulatory_report.description1')}</p>
-          <div className="markdown">
-            <ul>
-              <li>{translate('regulatory_report.bullet_point1')}</li>
-              <li>{translate('regulatory_report.bullet_point2')}</li>
-              <li>{translate('regulatory_report.bullet_point3')}</li>
-            </ul>
-          </div>
-          <p>{translate('regulatory_report.description2')}</p>
-          {branchOptions.length > 0 ? (
-            <>
-              <div className="modal-field big-spacer-top">
-                <label htmlFor="regulatory-report-branch-select">
-                  {translate('regulatory_page.select_branch')}
-                </label>
-                <Select
-                  className="width-100"
-                  inputId="regulatory-report-branch-select"
-                  id="regulatory-report-branch-select-input"
-                  onChange={this.onBranchSelect}
-                  options={branchOptions}
-                  value={branchOptions.find((o) => o.value === selectedBranch)}
-                />
-              </div>
-              <Alert variant="info">
-                <div>
-                  {translate('regulatory_page.available_branches_info.only_keep_when_inactive')}
-                </div>
-                <div>
-                  <FormattedMessage
-                    id="regulatory_page.available_branches_info.more_info"
-                    defaultMessage={translate('regulatory_page.available_branches_info.more_info')}
-                    values={{
-                      doc_link: (
-                        <DocLink to="/analyzing-source-code/branches/branch-analysis/#inactive-branches">
-                          {translate('regulatory_page.available_branches_info.more_info.doc_link')}
-                        </DocLink>
-                      ),
-                    }}
-                  />
-                </div>
-              </Alert>
-            </>
-          ) : (
-            <div className="big-spacer-top">
-              <Alert variant="warning">
-                <div>{translate('regulatory_page.no_available_branch')}</div>
-              </Alert>
-            </div>
-          )}
-          <div className="modal-field big-spacer-top">
-            {downloadStarted && (
-              <div>
-                <p>{translate('regulatory_page.download_start.sentence')}</p>
-              </div>
-            )}
-          </div>
-        </div>
-        <div className="modal-foot">
-          <a
-            className={classNames('button button-primary big-spacer-right', {
-              disabled: isDownloadButtonDisabled,
-            })}
-            download={[component.name, selectedBranch, 'regulatory report.zip']
-              .filter((s) => !!s)
-              .join(' - ')}
-            onClick={() => this.setState({ downloadStarted: true })}
-            href={getRegulatoryReportUrl(component.key, selectedBranch)}
-            target="_blank"
-            rel="noopener noreferrer"
-            aria-disabled={isDownloadButtonDisabled}
-          >
-            {translate('download_verb')}
-          </a>
-          <ButtonLink onClick={onClose}>{translate('close')}</ButtonLink>
-        </div>
-      </>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/RegulatoryReportModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/RegulatoryReportModal.tsx
deleted file mode 100644 (file)
index 9556833..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { translate } from '../../../../../../helpers/l10n';
-import { Component } from '../../../../../../types/types';
-import Modal from '../../../../../../components/controls/Modal';
-import RegulatoryReport from './RegulatoryReport';
-import ClickEventBoundary from '../../../../../../components/controls/ClickEventBoundary';
-import { BranchLike } from '../../../../../../types/branch-like';
-
-interface Props {
-  component: Component;
-  branchLike?: BranchLike;
-  onClose: () => void;
-}
-
-export default function RegulatoryReportModal(props: Props) {
-  const { component, branchLike } = props;
-  return (
-    <Modal contentLabel={translate('regulatory_report.page')} onRequestClose={props.onClose}>
-      <ClickEventBoundary>
-        <form>
-          <RegulatoryReport component={component} branchLike={branchLike} onClose={props.onClose} />
-        </form>
-      </ClickEventBoundary>
-    </Modal>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/__tests__/RegulatoryReport-it.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/__tests__/RegulatoryReport-it.tsx
deleted file mode 100644 (file)
index 00bec0d..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { mockBranch, mockMainBranch } from '../../../../../../../helpers/mocks/branch-like';
-import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
-import { BranchLike } from '../../../../../../../types/branch-like';
-import RegulatoryReport from '../RegulatoryReport';
-
-let handler: BranchesServiceMock;
-
-beforeAll(() => {
-  handler = new BranchesServiceMock();
-});
-
-afterEach(() => handler.reset());
-
-describe('RegulatoryReport tests', () => {
-  it('should open the regulatory report page', async () => {
-    const user = userEvent.setup();
-    renderRegulatoryReportApp();
-    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
-    expect(screen.getByText('regulatory_report.description1')).toBeInTheDocument();
-    expect(screen.getByText('regulatory_report.description2')).toBeInTheDocument();
-    expect(
-      screen.getByText('regulatory_page.available_branches_info.only_keep_when_inactive')
-    ).toBeInTheDocument();
-    expect(
-      screen.getByText('regulatory_page.available_branches_info.more_info')
-    ).toBeInTheDocument();
-
-    const branchSelect = screen.getByRole('combobox', { name: 'regulatory_page.select_branch' });
-    expect(branchSelect).toBeInTheDocument();
-
-    await user.click(branchSelect);
-    await user.keyboard('[ArrowDown][Enter]');
-
-    const downloadButton = screen.getByRole('link', { name: 'download_verb' });
-    expect(downloadButton).toBeInTheDocument();
-
-    expect(screen.queryByText('regulatory_page.download_start.sentence')).not.toBeInTheDocument();
-    await user.click(downloadButton);
-    expect(screen.getByText('regulatory_page.download_start.sentence')).toBeInTheDocument();
-  });
-
-  it('should display warning message if there is no available branch', async () => {
-    handler.emptyBranches();
-    renderRegulatoryReportApp();
-
-    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
-
-    expect(screen.getByText('regulatory_page.no_available_branch')).toBeInTheDocument();
-
-    const downloadButton = screen.getByRole('link', { name: 'download_verb' });
-    expect(downloadButton).toBeInTheDocument();
-    expect(downloadButton).toHaveClass('disabled');
-  });
-
-  it('should automatically select passed branch if compatible', async () => {
-    const compatibleBranch = mockBranch({ name: 'compatible-branch' });
-    handler.addBranch(compatibleBranch);
-    renderRegulatoryReportApp(compatibleBranch);
-
-    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
-
-    const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
-    expect(downloadButton).toBeInTheDocument();
-    expect(downloadButton).not.toHaveClass('disabled');
-    expect(downloadButton.href).toContain(compatibleBranch.name);
-  });
-
-  it('should automatically select main branch if present and passed branch is not compatible', async () => {
-    handler.emptyBranches();
-    const mainBranch = mockMainBranch({ name: 'main' });
-    const notCompatibleBranch = mockBranch({
-      name: 'not-compatible-branch',
-      excludedFromPurge: false,
-    });
-    handler.addBranch(mainBranch);
-    handler.addBranch(notCompatibleBranch);
-    renderRegulatoryReportApp(notCompatibleBranch);
-
-    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
-
-    const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
-    expect(downloadButton).toBeInTheDocument();
-    expect(downloadButton).not.toHaveClass('disabled');
-    expect(downloadButton.href).toContain(mainBranch.name);
-  });
-});
-
-function renderRegulatoryReportApp(branchLike?: BranchLike) {
-  renderComponent(
-    <RegulatoryReport
-      component={{ key: '', name: '' }}
-      branchLike={branchLike}
-      onClose={() => {}}
-    />
-  );
-}
index 6ecd1108c13f63c5bd79cf915b01820fa57a9f0b..1664ad7abcc56fb7103c0f174cfed8e8ca784ff8 100644 (file)
@@ -44,6 +44,7 @@ import projectBaselineRoutes from '../../apps/projectBaseline/routes';
 import projectBranchesRoutes from '../../apps/projectBranches/routes';
 import ProjectDeletionApp from '../../apps/projectDeletion/App';
 import projectDumpRoutes from '../../apps/projectDump/routes';
+import projectInfoRoutes from '../../apps/projectInformation/routes';
 import ProjectKeyApp from '../../apps/projectKey/ProjectKeyApp';
 import ProjectLinksApp from '../../apps/projectLinks/ProjectLinksApp';
 import projectQualityGateRoutes from '../../apps/projectQualityGate/routes';
@@ -114,6 +115,7 @@ function renderComponentRoutes() {
         <Route path="security_hotspots" element={<SecurityHotspotsApp />} />
         {projectQualityGateRoutes()}
         {projectQualityProfilesRoutes()}
+        {projectInfoRoutes()}
 
         {tutorialsRoutes()}
       </Route>
index 649e40a2287c648fbae81d6478090407566c09cb..93899de2a3fb07175e58b858e688a1d2c86ea99d 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import MetaLink from '../../../app/components/nav/component/projectInformation/meta/MetaLink';
 import Link from '../../../components/common/Link';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import DateFromNow from '../../../components/intl/DateFromNow';
@@ -27,6 +26,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { orderLinks } from '../../../helpers/projectLinks';
 import { getProjectUrl } from '../../../helpers/urls';
 import { MyProject, ProjectLink } from '../../../types/types';
+import MetaLink from '../../projectInformation/meta/MetaLink';
 
 interface Props {
   project: MyProject;
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx b/server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx
new file mode 100644 (file)
index 0000000..08dabaf
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { Card, LargeCenteredLayout, PageContentFontWrapper, Title } from 'design-system';
+import * as React from 'react';
+import { getMeasures } from '../../api/measures';
+import withAvailableFeatures, {
+  WithAvailableFeaturesProps,
+} from '../../app/components/available-features/withAvailableFeatures';
+import withComponentContext from '../../app/components/componentContext/withComponentContext';
+import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
+import withMetricsContext from '../../app/components/metrics/withMetricsContext';
+import { translate } from '../../helpers/l10n';
+import { BranchLike } from '../../types/branch-like';
+import { ComponentQualifier } from '../../types/component';
+import { Feature } from '../../types/features';
+import { MetricKey } from '../../types/metrics';
+import { Component, Dict, Measure, Metric } from '../../types/types';
+import { CurrentUser, isLoggedIn } from '../../types/users';
+import AboutProject from './about/AboutProject';
+import ProjectBadges from './badges/ProjectBadges';
+import ProjectNotifications from './notifications/ProjectNotifications';
+import RegulatoryReport from './projectRegulatoryReport/RegulatoryReport';
+
+interface Props extends WithAvailableFeaturesProps {
+  branchLike?: BranchLike;
+  component: Component;
+  currentUser: CurrentUser;
+  onComponentChange: (changes: {}) => void;
+  metrics: Dict<Metric>;
+}
+
+interface State {
+  measures?: Measure[];
+}
+
+export class ProjectInformationApp extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = {};
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadMeasures();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  loadMeasures = () => {
+    const {
+      component: { key },
+    } = this.props;
+
+    return getMeasures({
+      component: key,
+      metricKeys: [MetricKey.ncloc, MetricKey.projects].join(),
+    }).then((measures) => {
+      if (this.mounted) {
+        this.setState({ measures });
+      }
+    });
+  };
+
+  render() {
+    const { branchLike, component, currentUser, metrics } = this.props;
+    const { measures } = this.state;
+
+    const canConfigureNotifications =
+      isLoggedIn(currentUser) && component.qualifier === ComponentQualifier.Project;
+    const canUseBadges =
+      metrics !== undefined &&
+      (component.qualifier === ComponentQualifier.Application ||
+        component.qualifier === ComponentQualifier.Project);
+    const regulatoryReportFeatureEnabled = this.props.hasFeature(Feature.RegulatoryReport);
+    const isApp = component.qualifier === ComponentQualifier.Application;
+
+    return (
+      <main>
+        <LargeCenteredLayout>
+          <PageContentFontWrapper>
+            <div className="overview sw-my-6 sw-body-sm">
+              <Title className="sw-mb-12">
+                {translate(isApp ? 'application' : 'project', 'info.title')}
+              </Title>
+              <div className="sw-grid sw-grid-cols-[488px_minmax(0,_2fr)] sw-gap-x-12 sw-gap-y-3 sw-auto-rows-min">
+                <div className="sw-row-span-3">
+                  <Card>
+                    <AboutProject
+                      component={component}
+                      measures={measures}
+                      onComponentChange={this.props.onComponentChange}
+                    />
+                  </Card>
+                </div>
+
+                {canConfigureNotifications && (
+                  <Card>
+                    <ProjectNotifications component={component} />
+                  </Card>
+                )}
+                {canUseBadges && (
+                  <Card>
+                    <ProjectBadges branchLike={branchLike} component={component} />
+                  </Card>
+                )}
+                {component.qualifier === ComponentQualifier.Project &&
+                  regulatoryReportFeatureEnabled && (
+                    <Card>
+                      <RegulatoryReport
+                        component={component}
+                        branchLike={branchLike}
+                        onClose={() => {}}
+                      />
+                    </Card>
+                  )}
+              </div>
+            </div>
+          </PageContentFontWrapper>
+        </LargeCenteredLayout>
+      </main>
+    );
+  }
+}
+
+export default withComponentContext(
+  withCurrentUserContext(withMetricsContext(withAvailableFeatures(ProjectInformationApp)))
+);
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx
new file mode 100644 (file)
index 0000000..5b2b9a3
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
+import { translate } from '../../../helpers/l10n';
+import { ComponentQualifier } from '../../../types/component';
+import { Component, Measure } from '../../../types/types';
+import MetaKey from '../meta/MetaKey';
+import MetaLinks from '../meta/MetaLinks';
+import MetaQualityGate from '../meta/MetaQualityGate';
+import MetaQualityProfiles from '../meta/MetaQualityProfiles';
+import MetaSize from '../meta/MetaSize';
+import MetaTags from '../meta/MetaTags';
+
+export interface AboutProjectProps {
+  component: Component;
+  measures?: Measure[];
+  onComponentChange: (changes: {}) => void;
+}
+
+export function AboutProject(props: AboutProjectProps) {
+  const { component, measures = [] } = props;
+
+  const heading = React.useRef<HTMLHeadingElement>(null);
+  const isApp = component.qualifier === ComponentQualifier.Application;
+
+  React.useEffect(() => {
+    if (heading.current) {
+      // a11y: provide focus to the heading when the Project Information is opened.
+      heading.current.focus();
+    }
+  }, [heading]);
+
+  return (
+    <>
+      <div>
+        <h2 className="big-padded bordered-bottom" tabIndex={-1} ref={heading}>
+          {translate(isApp ? 'application' : 'project', 'info.title')}
+        </h2>
+      </div>
+
+      <div className="overflow-y-auto">
+        <div className="big-padded bordered-bottom">
+          <div className="display-flex-center">
+            <h3 className="spacer-right">{translate('project.info.description')}</h3>
+            {component.visibility && (
+              <PrivacyBadgeContainer
+                qualifier={component.qualifier}
+                visibility={component.visibility}
+              />
+            )}
+          </div>
+
+          {component.description && (
+            <p className="it__project-description">{component.description}</p>
+          )}
+
+          <MetaTags component={component} onComponentChange={props.onComponentChange} />
+        </div>
+
+        <div className="big-padded bordered-bottom it__project-loc-value">
+          <MetaSize component={component} measures={measures} />
+        </div>
+
+        {!isApp &&
+          (component.qualityGate ||
+            (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
+            <div className="big-padded bordered-bottom">
+              {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
+
+              {component.qualityProfiles && component.qualityProfiles.length > 0 && (
+                <MetaQualityProfiles
+                  headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
+                  profiles={component.qualityProfiles}
+                />
+              )}
+            </div>
+          )}
+
+        {!isApp && <MetaLinks component={component} />}
+
+        <div className="big-padded bordered-bottom">
+          <MetaKey componentKey={component.key} qualifier={component.qualifier} />
+        </div>
+      </div>
+    </>
+  );
+}
+
+export default AboutProject;
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeButton.tsx b/server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeButton.tsx
new file mode 100644 (file)
index 0000000..daea2e4
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 * as React from 'react';
+import { Button } from '../../../components/controls/buttons';
+import { translate } from '../../../helpers/l10n';
+import { BadgeType } from './utils';
+
+interface Props {
+  onClick: (type: BadgeType) => void;
+  selected: boolean;
+  type: BadgeType;
+  url: string;
+}
+
+export default class BadgeButton extends React.PureComponent<Props> {
+  handleClick = () => {
+    this.props.onClick(this.props.type);
+  };
+
+  render() {
+    const { selected, type, url } = this.props;
+    const width = type !== BadgeType.measure ? '128px' : undefined;
+    return (
+      <Button className={classNames('badge-button', { selected })} onClick={this.handleClick}>
+        <img alt={translate('overview.badges', type, 'alt')} src={url} width={width} />
+      </Button>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeParams.tsx b/server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeParams.tsx
new file mode 100644 (file)
index 0000000..c8789b0
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 * as React from 'react';
+import { fetchWebApi } from '../../../api/web-api';
+import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
+import Select from '../../../components/controls/Select';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { Dict, Metric } from '../../../types/types';
+import { BadgeFormats, BadgeOptions, BadgeType } from './utils';
+
+interface Props {
+  className?: string;
+  metrics: Dict<Metric>;
+  options: BadgeOptions;
+  type: BadgeType;
+  updateOptions: (options: Partial<BadgeOptions>) => void;
+}
+
+interface State {
+  badgeMetrics: string[];
+}
+
+export class BadgeParams extends React.PureComponent<Props> {
+  mounted = false;
+
+  state: State = { badgeMetrics: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchBadgeMetrics();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchBadgeMetrics() {
+    fetchWebApi(false).then(
+      (webservices) => {
+        if (this.mounted) {
+          const domain = webservices.find((d) => d.path === 'api/project_badges');
+          const ws = domain && domain.actions.find((w) => w.key === 'measure');
+          const param = ws && ws.params && ws.params.find((p) => p.key === 'metric');
+          if (param && param.possibleValues) {
+            this.setState({ badgeMetrics: param.possibleValues });
+          }
+        }
+      },
+      () => {}
+    );
+  }
+
+  getColorOptions = () => {
+    return ['white', 'black', 'orange'].map((color) => ({
+      label: translate('overview.badges.options.colors', color),
+      value: color,
+    }));
+  };
+
+  getFormatOptions = () => {
+    return ['md', 'url'].map((format) => ({
+      label: translate('overview.badges.options.formats', format),
+      value: format as BadgeFormats,
+    }));
+  };
+
+  getMetricOptions = () => {
+    return this.state.badgeMetrics.map((key) => {
+      const metric = this.props.metrics[key];
+      return {
+        value: key,
+        label: metric ? getLocalizedMetricName(metric) : key,
+      };
+    });
+  };
+
+  handleFormatChange = ({ value }: { value: BadgeFormats }) => {
+    this.props.updateOptions({ format: value });
+  };
+
+  handleMetricChange = ({ value }: { value: string }) => {
+    this.props.updateOptions({ metric: value });
+  };
+
+  renderBadgeType = (type: BadgeType, options: BadgeOptions) => {
+    if (type === BadgeType.measure) {
+      const metricOptions = this.getMetricOptions();
+      return (
+        <>
+          <label className="spacer-right" htmlFor="badge-metric">
+            {translate('overview.badges.metric')}:
+          </label>
+          <Select
+            className="input-medium it__metric-badge-select"
+            inputId="badge-metric"
+            isSearchable={false}
+            onChange={this.handleMetricChange}
+            options={metricOptions}
+            value={metricOptions.find((o) => o.value === options.metric)}
+          />
+        </>
+      );
+    }
+    return null;
+  };
+
+  render() {
+    const { className, options, type } = this.props;
+    const formatOptions = this.getFormatOptions();
+    return (
+      <div className={className}>
+        {this.renderBadgeType(type, options)}
+
+        <label
+          className={classNames('spacer-right', {
+            'spacer-top': type !== BadgeType.qualityGate,
+          })}
+          htmlFor="badge-format"
+        >
+          {translate('format')}:
+        </label>
+        <Select
+          className="input-medium"
+          inputId="badge-format"
+          isSearchable={false}
+          onChange={this.handleFormatChange}
+          options={formatOptions}
+          value={formatOptions.find((o) => o.value === options.format)}
+          defaultValue={formatOptions.find((o) => o.value === 'md')}
+        />
+      </div>
+    );
+  }
+}
+
+export default withMetricsContext(BadgeParams);
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx b/server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx
new file mode 100644 (file)
index 0000000..4b4a8ff
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { getProjectBadgesToken, renewProjectBadgesToken } from '../../../api/project-badges';
+import CodeSnippet from '../../../components/common/CodeSnippet';
+import { Button } from '../../../components/controls/buttons';
+import { Alert } from '../../../components/ui/Alert';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
+import { Component } from '../../../types/types';
+import BadgeButton from './BadgeButton';
+import BadgeParams from './BadgeParams';
+import './styles.css';
+import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: Component;
+}
+
+interface State {
+  isRenewing: boolean;
+  token: string;
+  selectedType: BadgeType;
+  badgeOptions: BadgeOptions;
+}
+
+export default class ProjectBadges extends React.PureComponent<Props, State> {
+  mounted = false;
+  headingNodeRef = React.createRef<HTMLHeadingElement>();
+  state: State = {
+    isRenewing: false,
+    token: '',
+    selectedType: BadgeType.measure,
+    badgeOptions: { metric: MetricKey.alert_status },
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchToken();
+    if (this.headingNodeRef.current) {
+      this.headingNodeRef.current.focus();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  async fetchToken() {
+    const {
+      component: { key },
+    } = this.props;
+    const token = await getProjectBadgesToken(key).catch(() => '');
+    if (this.mounted) {
+      this.setState({ token });
+    }
+  }
+
+  handleSelectBadge = (selectedType: BadgeType) => {
+    this.setState({ selectedType });
+  };
+
+  handleUpdateOptions = (options: Partial<BadgeOptions>) => {
+    this.setState((state) => ({
+      badgeOptions: { ...state.badgeOptions, ...options },
+    }));
+  };
+
+  handleRenew = async () => {
+    const {
+      component: { key },
+    } = this.props;
+
+    this.setState({ isRenewing: true });
+    await renewProjectBadgesToken(key).catch(() => {});
+    await this.fetchToken();
+    if (this.mounted) {
+      this.setState({ isRenewing: false });
+    }
+  };
+
+  render() {
+    const {
+      branchLike,
+      component: { key: project, qualifier, configuration },
+    } = this.props;
+    const { isRenewing, selectedType, badgeOptions, token } = this.state;
+    const fullBadgeOptions = {
+      project,
+      ...badgeOptions,
+      ...getBranchLikeQuery(branchLike),
+    };
+    const canRenew = configuration?.showSettings;
+
+    return (
+      <div className="display-flex-column">
+        <h3 tabIndex={-1} ref={this.headingNodeRef}>
+          {translate('overview.badges.get_badge', qualifier)}
+        </h3>
+        <p className="big-spacer-bottom">{translate('overview.badges.description', qualifier)}</p>
+        <BadgeButton
+          onClick={this.handleSelectBadge}
+          selected={BadgeType.measure === selectedType}
+          type={BadgeType.measure}
+          url={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
+        />
+        <p className="huge-spacer-bottom spacer-top">
+          {translate('overview.badges', BadgeType.measure, 'description', qualifier)}
+        </p>
+        <BadgeButton
+          onClick={this.handleSelectBadge}
+          selected={BadgeType.qualityGate === selectedType}
+          type={BadgeType.qualityGate}
+          url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
+        />
+        <p className="huge-spacer-bottom spacer-top">
+          {translate('overview.badges', BadgeType.qualityGate, 'description', qualifier)}
+        </p>
+        <BadgeParams
+          className="big-spacer-bottom display-flex-column"
+          options={badgeOptions}
+          type={selectedType}
+          updateOptions={this.handleUpdateOptions}
+        />
+        {isRenewing ? (
+          <div className="spacer-top spacer-bottom display-flex-row display-flex-justify-center">
+            <DeferredSpinner className="spacer-top spacer-bottom" loading={isRenewing} />
+          </div>
+        ) : (
+          <CodeSnippet isOneLine snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} />
+        )}
+
+        <Alert variant="warning">
+          <p>
+            {translate('overview.badges.leak_warning')}{' '}
+            {canRenew && translate('overview.badges.renew.description')}
+          </p>
+          {canRenew && (
+            <Button
+              disabled={isRenewing}
+              className="spacer-top it__project-info-renew-badge"
+              onClick={this.handleRenew}
+            >
+              {translate('overview.badges.renew')}
+            </Button>
+          )}
+        </Alert>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-test.tsx
new file mode 100644 (file)
index 0000000..28e3bfb
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 selectEvent from 'react-select-event';
+import { getProjectBadgesToken } from '../../../../api/project-badges';
+import { mockBranch } from '../../../../helpers/mocks/branch-like';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { Location } from '../../../../helpers/urls';
+import { ComponentQualifier } from '../../../../types/component';
+import { MetricKey } from '../../../../types/metrics';
+import ProjectBadges from '../ProjectBadges';
+import { BadgeType } from '../utils';
+
+jest.mock('../../../../helpers/urls', () => ({
+  getHostUrl: () => 'host',
+  getPathUrlAsString: (l: Location) => l.pathname,
+  getProjectUrl: () => ({ pathname: '/dashboard' } as Location),
+}));
+
+jest.mock('../../../../api/project-badges', () => ({
+  getProjectBadgesToken: jest.fn().mockResolvedValue('foo'),
+  renewProjectBadgesToken: jest.fn().mockResolvedValue({}),
+}));
+
+jest.mock('../../../../api/web-api', () => ({
+  fetchWebApi: () =>
+    Promise.resolve([
+      {
+        path: 'api/project_badges',
+        actions: [
+          {
+            key: 'measure',
+            // eslint-disable-next-line local-rules/use-metrickey-enum
+            params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage'] }],
+          },
+        ],
+      },
+    ]),
+}));
+
+it('should renew token', async () => {
+  const user = userEvent.setup();
+  jest.mocked(getProjectBadgesToken).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar');
+  renderProjectBadges({
+    component: mockComponent({ configuration: { showSettings: true } }),
+  });
+
+  expect(
+    await screen.findByText(`overview.badges.get_badge.${ComponentQualifier.Project}`)
+  ).toHaveFocus();
+
+  expect(screen.getByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)).toHaveAttribute(
+    'src',
+    'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo'
+  );
+
+  expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
+    'src',
+    'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo'
+  );
+
+  await user.click(screen.getByText('overview.badges.renew'));
+
+  expect(
+    await screen.findByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)
+  ).toHaveAttribute(
+    'src',
+    'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar'
+  );
+
+  expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
+    'src',
+    'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=bar'
+  );
+});
+
+it('should update params', async () => {
+  renderProjectBadges({
+    component: mockComponent({ configuration: { showSettings: true } }),
+  });
+
+  expect(
+    await screen.findByText(
+      '[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo)](/dashboard)'
+    )
+  ).toBeInTheDocument();
+
+  await selectEvent.select(screen.getByLabelText('format:'), [
+    'overview.badges.options.formats.url',
+  ]);
+
+  expect(
+    screen.getByText(
+      'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo'
+    )
+  ).toBeInTheDocument();
+
+  await selectEvent.select(screen.getByLabelText('overview.badges.metric:'), MetricKey.coverage);
+
+  expect(
+    screen.getByText(
+      `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo`
+    )
+  ).toBeInTheDocument();
+});
+
+function renderProjectBadges(props: Partial<ProjectBadges['props']> = {}) {
+  return renderComponent(
+    <ProjectBadges
+      branchLike={mockBranch()}
+      component={mockComponent({ key: 'foo', qualifier: ComponentQualifier.Project })}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/utils-test.ts
new file mode 100644 (file)
index 0000000..359d288
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { Location } from '../../../../components/hoc/withRouter';
+import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from '../utils';
+
+jest.mock('../../../../helpers/urls', () => ({
+  ...jest.requireActual('../../../../helpers/urls'),
+  getHostUrl: () => 'host',
+  getPathUrlAsString: (o: Location) => `host${o.pathname}${o.search}`,
+}));
+
+const options: BadgeOptions = {
+  branch: 'master',
+  metric: 'alert_status',
+  project: 'foo',
+};
+
+describe('#getBadgeUrl', () => {
+  it('should generate correct quality gate badge links', () => {
+    expect(getBadgeUrl(BadgeType.qualityGate, options, 'foo')).toBe(
+      'host/api/project_badges/quality_gate?branch=master&project=foo&token=foo'
+    );
+  });
+
+  it('should generate correct measures badge links', () => {
+    expect(getBadgeUrl(BadgeType.measure, options, 'foo')).toBe(
+      'host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo'
+    );
+  });
+
+  it('should ignore undefined parameters', () => {
+    expect(getBadgeUrl(BadgeType.measure, { metric: 'alert_status' }, 'foo')).toBe(
+      'host/api/project_badges/measure?metric=alert_status&token=foo'
+    );
+  });
+
+  it('should force metric parameters', () => {
+    expect(getBadgeUrl(BadgeType.measure, {}, 'foo')).toBe(
+      'host/api/project_badges/measure?metric=alert_status&token=foo'
+    );
+  });
+});
+
+describe('#getBadgeSnippet', () => {
+  it('should generate a correct markdown image', () => {
+    expect(getBadgeSnippet(BadgeType.measure, { ...options, format: 'md' }, 'foo')).toBe(
+      '[![alert_status](host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo)](host/dashboard?id=foo&branch=master)'
+    );
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/styles.css b/server/sonar-web/src/main/js/apps/projectInformation/badges/styles.css
new file mode 100644 (file)
index 0000000..8dca6fd
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.
+ */
+.badges-list {
+  display: flex;
+  justify-content: space-around;
+  justify-content: space-evenly;
+  flex-wrap: nowrap;
+}
+
+.button.badge-button {
+  display: flex;
+  justify-content: center;
+  padding: var(--gridSize);
+  min-width: 146px;
+  height: 116px;
+  background-color: var(--barBackgroundColor);
+  border: solid 1px var(--barBorderColor);
+  border-radius: 3px;
+  transition: all 0.3s ease;
+}
+
+.button.badge-button:hover,
+.button.badge-button:focus,
+.button.badge-button:active {
+  background-color: var(--barBackgroundColor);
+  border-color: var(--blue);
+}
+
+.button.badge-button.selected {
+  background-color: var(--lightBlue);
+  border-color: var(--darkBlue);
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/utils.ts b/server/sonar-web/src/main/js/apps/projectInformation/badges/utils.ts
new file mode 100644 (file)
index 0000000..c43e9c6
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { getLocalizedMetricName } from '../../../helpers/l10n';
+import { omitNil } from '../../../helpers/request';
+import { getHostUrl, getPathUrlAsString, getProjectUrl } from '../../../helpers/urls';
+
+export type BadgeColors = 'white' | 'black' | 'orange';
+export type BadgeFormats = 'md' | 'url';
+
+export interface BadgeOptions {
+  branch?: string;
+  format?: BadgeFormats;
+  project?: string;
+  metric?: string;
+  pullRequest?: string;
+}
+
+export enum BadgeType {
+  measure = 'measure',
+  qualityGate = 'quality_gate',
+}
+
+export function getBadgeSnippet(type: BadgeType, options: BadgeOptions, token: string) {
+  const url = getBadgeUrl(type, options, token);
+  const { branch, format = 'md', metric = 'alert_status', project } = options;
+
+  if (format === 'url') {
+    return url;
+  }
+
+  let label;
+  let projectUrl;
+
+  switch (type) {
+    case BadgeType.measure:
+      label = getLocalizedMetricName({ key: metric });
+      break;
+    case BadgeType.qualityGate:
+    default:
+      label = 'Quality gate';
+      break;
+  }
+
+  if (project) {
+    projectUrl = getPathUrlAsString(getProjectUrl(project, branch), false);
+  }
+
+  const mdImage = `![${label}](${url})`;
+  return projectUrl ? `[${mdImage}](${projectUrl})` : mdImage;
+}
+
+export function getBadgeUrl(
+  type: BadgeType,
+  { branch, project, metric = 'alert_status', pullRequest }: BadgeOptions,
+  token: string
+) {
+  switch (type) {
+    case BadgeType.qualityGate:
+      return `${getHostUrl()}/api/project_badges/quality_gate?${new URLSearchParams(
+        omitNil({ branch, project, pullRequest, token })
+      ).toString()}`;
+    case BadgeType.measure:
+    default:
+      return `${getHostUrl()}/api/project_badges/measure?${new URLSearchParams(
+        omitNil({ branch, project, metric, pullRequest, token })
+      ).toString()}`;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaKey.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaKey.tsx
new file mode 100644 (file)
index 0000000..8532fac
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { ClipboardButton } from '../../../components/controls/clipboard';
+import { translate } from '../../../helpers/l10n';
+
+export interface MetaKeyProps {
+  componentKey: string;
+  qualifier: string;
+}
+
+export default function MetaKey({ componentKey, qualifier }: MetaKeyProps) {
+  return (
+    <>
+      <h3 id="project-key">{translate('overview.project_key', qualifier)}</h3>
+      <div className="display-flex-center">
+        <input
+          className="overview-key"
+          aria-labelledby="project-key"
+          readOnly
+          type="text"
+          value={componentKey}
+        />
+        <ClipboardButton
+          aria-label={translate('overview.project_key.click_to_copy')}
+          className="little-spacer-left"
+          copyValue={componentKey}
+        />
+      </div>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLink.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLink.tsx
new file mode 100644 (file)
index 0000000..e81f53c
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 isValidUri from '../../../app/utils/isValidUri';
+import { ClearButton } from '../../../components/controls/buttons';
+import ProjectLinkIcon from '../../../components/icons/ProjectLinkIcon';
+import { getLinkName } from '../../../helpers/projectLinks';
+import { ProjectLink } from '../../../types/types';
+
+interface Props {
+  iconOnly?: boolean;
+  link: ProjectLink;
+}
+
+interface State {
+  expanded: boolean;
+}
+
+export default class MetaLink extends React.PureComponent<Props, State> {
+  state = { expanded: false };
+
+  handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.setState(({ expanded }) => ({ expanded: !expanded }));
+  };
+
+  handleCollapse = () => {
+    this.setState({ expanded: false });
+  };
+
+  handleSelect = (event: React.MouseEvent<HTMLInputElement>) => {
+    event.currentTarget.select();
+  };
+
+  render() {
+    const { iconOnly, link } = this.props;
+    const linkTitle = getLinkName(link);
+    const isValid = isValidUri(link.url);
+    return (
+      <li>
+        <a
+          className="link-no-underline"
+          href={isValid ? link.url : undefined}
+          onClick={isValid ? undefined : this.handleClick}
+          rel="nofollow noreferrer noopener"
+          target="_blank"
+          title={linkTitle}
+        >
+          <ProjectLinkIcon className="little-spacer-right" type={link.type} />
+          {!iconOnly && linkTitle}
+        </a>
+        {this.state.expanded && (
+          <div className="little-spacer-top display-flex-center">
+            <input
+              className="overview-key width-80"
+              onClick={this.handleSelect}
+              readOnly
+              type="text"
+              value={link.url}
+            />
+            <ClearButton className="little-spacer-left" onClick={this.handleCollapse} />
+          </div>
+        )}
+      </li>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLinks.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLinks.tsx
new file mode 100644 (file)
index 0000000..c6df449
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { getProjectLinks } from '../../../api/projectLinks';
+import { translate } from '../../../helpers/l10n';
+import { orderLinks } from '../../../helpers/projectLinks';
+import { LightComponent, ProjectLink } from '../../../types/types';
+import MetaLink from './MetaLink';
+
+interface Props {
+  component: LightComponent;
+}
+
+interface State {
+  links?: ProjectLink[];
+}
+
+export default class MetaLinks extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = {};
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadLinks();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.component.key !== this.props.component.key) {
+      this.loadLinks();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  loadLinks = () =>
+    getProjectLinks(this.props.component.key).then(
+      (links) => {
+        if (this.mounted) {
+          this.setState({ links });
+        }
+      },
+      () => {}
+    );
+
+  render() {
+    const { links } = this.state;
+
+    if (!links || links.length === 0) {
+      return null;
+    }
+
+    const orderedLinks = orderLinks(links);
+
+    return (
+      <div className="big-padded bordered-bottom">
+        <h3>{translate('overview.external_links')}</h3>
+        <ul className="project-info-list">
+          {orderedLinks.map((link) => (
+            <MetaLink key={link.id} link={link} />
+          ))}
+        </ul>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityGate.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityGate.tsx
new file mode 100644 (file)
index 0000000..94719fc
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 } from '../../../helpers/l10n';
+import { getQualityGateUrl } from '../../../helpers/urls';
+
+interface Props {
+  qualityGate: { isDefault?: boolean; name: string };
+}
+
+export default function MetaQualityGate({ qualityGate }: Props) {
+  return (
+    <>
+      <h3>{translate('project.info.quality_gate')}</h3>
+
+      <ul className="project-info-list">
+        <li>
+          {qualityGate.isDefault && (
+            <span className="note spacer-right">({translate('default')})</span>
+          )}
+          <Link to={getQualityGateUrl(qualityGate.name)}>{qualityGate.name}</Link>
+        </li>
+      </ul>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityProfiles.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityProfiles.tsx
new file mode 100644 (file)
index 0000000..1788efd
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { searchRules } from '../../../api/rules';
+import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
+import Link from '../../../components/common/Link';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { getQualityProfileUrl } from '../../../helpers/urls';
+import { Languages } from '../../../types/languages';
+import { ComponentQualityProfile, Dict } from '../../../types/types';
+
+interface Props {
+  headerClassName?: string;
+  languages: Languages;
+  profiles: ComponentQualityProfile[];
+}
+
+interface State {
+  deprecatedByKey: Dict<number>;
+}
+
+export class MetaQualityProfiles extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { deprecatedByKey: {} };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadDeprecatedRules();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  loadDeprecatedRules() {
+    const existingProfiles = this.props.profiles.filter((p) => !p.deleted);
+    const requests = existingProfiles.map((profile) =>
+      this.loadDeprecatedRulesForProfile(profile.key)
+    );
+    Promise.all(requests).then(
+      (responses) => {
+        if (this.mounted) {
+          const deprecatedByKey: Dict<number> = {};
+          responses.forEach((count, i) => {
+            const profileKey = existingProfiles[i].key;
+            deprecatedByKey[profileKey] = count;
+          });
+          this.setState({ deprecatedByKey });
+        }
+      },
+      () => {}
+    );
+  }
+
+  loadDeprecatedRulesForProfile(profileKey: string) {
+    const data = {
+      activation: 'true',
+      ps: 1,
+      qprofile: profileKey,
+      statuses: 'DEPRECATED',
+    };
+    return searchRules(data).then((r) => r.paging.total);
+  }
+
+  getDeprecatedRulesCount(profile: { key: string }) {
+    const count = this.state.deprecatedByKey[profile.key];
+    return count || 0;
+  }
+
+  renderProfile(profile: ComponentQualityProfile) {
+    const languageFromStore = this.props.languages[profile.language];
+    const languageName = languageFromStore ? languageFromStore.name : profile.language;
+
+    const inner = (
+      <div className="text-ellipsis">
+        <span className="spacer-right">({languageName})</span>
+        {profile.deleted ? (
+          profile.name
+        ) : (
+          <Link to={getQualityProfileUrl(profile.name, profile.language)}>
+            <span
+              aria-label={translateWithParameters(
+                'overview.link_to_x_profile_y',
+                languageName,
+                profile.name
+              )}
+            >
+              {profile.name}
+            </span>
+          </Link>
+        )}
+      </div>
+    );
+
+    if (profile.deleted) {
+      const tooltip = translateWithParameters('overview.deleted_profile', profile.name);
+      return (
+        <Tooltip key={profile.key} overlay={tooltip}>
+          <li className="project-info-deleted-profile">{inner}</li>
+        </Tooltip>
+      );
+    }
+
+    const count = this.getDeprecatedRulesCount(profile);
+
+    if (count > 0) {
+      const tooltip = translateWithParameters('overview.deprecated_profile', count);
+      return (
+        <Tooltip key={profile.key} overlay={tooltip}>
+          <li className="project-info-deprecated-rules">{inner}</li>
+        </Tooltip>
+      );
+    }
+
+    return <li key={profile.key}>{inner}</li>;
+  }
+
+  render() {
+    const { headerClassName, profiles } = this.props;
+
+    return (
+      <>
+        <h3 className={headerClassName}>{translate('overview.quality_profiles')}</h3>
+
+        <ul className="project-info-list">
+          {profiles.map((profile) => this.renderProfile(profile))}
+        </ul>
+      </>
+    );
+  }
+}
+
+export default withLanguagesContext(MetaQualityProfiles);
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaSize.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaSize.tsx
new file mode 100644 (file)
index 0000000..499d3b1
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 DrilldownLink from '../../../components/shared/DrilldownLink';
+import SizeRating from '../../../components/ui/SizeRating';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure, localizeMetric } from '../../../helpers/measures';
+import { ComponentQualifier } from '../../../types/component';
+import { MetricKey } from '../../../types/metrics';
+import { Component, Measure } from '../../../types/types';
+
+export interface MetaSizeProps {
+  component: Component;
+  measures: Measure[];
+}
+
+export default function MetaSize({ component, measures }: MetaSizeProps) {
+  const isApp = component.qualifier === ComponentQualifier.Application;
+  const ncloc = measures.find((measure) => measure.metric === MetricKey.ncloc);
+  const projects = isApp
+    ? measures.find((measure) => measure.metric === MetricKey.projects)
+    : undefined;
+
+  return (
+    <>
+      <div className="display-flex-row display-inline-flex-baseline">
+        <h3>{localizeMetric(MetricKey.ncloc)}</h3>
+        <span className="spacer-left small">({translate('project.info.main_branch')})</span>
+      </div>
+      <div className="display-flex-center">
+        {ncloc && ncloc.value ? (
+          <>
+            <DrilldownLink className="huge" component={component.key} metric={MetricKey.ncloc}>
+              <span
+                aria-label={translateWithParameters(
+                  'project.info.see_more_info_on_x_locs',
+                  ncloc.value
+                )}
+              >
+                {formatMeasure(ncloc.value, 'SHORT_INT')}
+              </span>
+            </DrilldownLink>
+
+            <span className="spacer-left">
+              <SizeRating value={Number(ncloc.value)} />
+            </span>
+          </>
+        ) : (
+          <span>0</span>
+        )}
+
+        {isApp && (
+          <span className="huge-spacer-left display-inline-flex-center">
+            {projects ? (
+              <DrilldownLink component={component.key} metric={MetricKey.projects}>
+                <span className="big">{formatMeasure(projects.value, 'SHORT_INT')}</span>
+              </DrilldownLink>
+            ) : (
+              <span className="big">0</span>
+            )}
+            <span className="little-spacer-left text-muted">
+              {translate('metric.projects.name')}
+            </span>
+          </span>
+        )}
+      </div>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTags.tsx
new file mode 100644 (file)
index 0000000..d12a0bf
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { setApplicationTags, setProjectTags } from '../../../api/components';
+import Dropdown from '../../../components/controls/Dropdown';
+import { ButtonLink } from '../../../components/controls/buttons';
+import TagsList from '../../../components/tags/TagsList';
+import { PopupPlacement } from '../../../components/ui/popups';
+import { translate } from '../../../helpers/l10n';
+import { ComponentQualifier } from '../../../types/component';
+import { Component } from '../../../types/types';
+import MetaTagsSelector from './MetaTagsSelector';
+
+interface Props {
+  component: Component;
+  onComponentChange: (changes: {}) => void;
+}
+
+export default class MetaTags extends React.PureComponent<Props> {
+  canUpdateTags = () => {
+    const { configuration } = this.props.component;
+    return configuration && configuration.showSettings;
+  };
+
+  setTags = (values: string[]) => {
+    const { component } = this.props;
+
+    if (component.qualifier === ComponentQualifier.Application) {
+      return setApplicationTags({
+        application: component.key,
+        tags: values.join(','),
+      });
+    }
+
+    return setProjectTags({
+      project: component.key,
+      tags: values.join(','),
+    });
+  };
+
+  handleSetProjectTags = (values: string[]) => {
+    this.setTags(values).then(
+      () => this.props.onComponentChange({ tags: values }),
+      () => {}
+    );
+  };
+
+  render() {
+    const tags = this.props.component.tags || [];
+
+    return this.canUpdateTags() ? (
+      <div className="big-spacer-top project-info-tags">
+        <Dropdown
+          closeOnClick={false}
+          closeOnClickOutside
+          overlay={
+            <MetaTagsSelector selectedTags={tags} setProjectTags={this.handleSetProjectTags} />
+          }
+          overlayPlacement={PopupPlacement.BottomLeft}
+        >
+          <ButtonLink stopPropagation>
+            <TagsList allowUpdate tags={tags.length ? tags : [translate('no_tags')]} />
+          </ButtonLink>
+        </Dropdown>
+      </div>
+    ) : (
+      <div className="big-spacer-top project-info-tags">
+        <TagsList
+          allowUpdate={false}
+          className="note"
+          tags={tags.length ? tags : [translate('no_tags')]}
+        />
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTagsSelector.tsx
new file mode 100644 (file)
index 0000000..d2602d4
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { difference, without } from 'lodash';
+import * as React from 'react';
+import { searchProjectTags } from '../../../api/components';
+import TagsSelector from '../../../components/tags/TagsSelector';
+
+interface Props {
+  selectedTags: string[];
+  setProjectTags: (tags: string[]) => void;
+}
+
+interface State {
+  searchResult: string[];
+}
+
+const LIST_SIZE = 10;
+
+export default class MetaTagsSelector extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { searchResult: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  onSearch = (query: string) => {
+    return searchProjectTags({
+      q: query,
+      ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
+    }).then(
+      ({ tags }) => {
+        if (this.mounted) {
+          this.setState({ searchResult: tags });
+        }
+      },
+      () => {}
+    );
+  };
+
+  onSelect = (tag: string) => {
+    this.props.setProjectTags([...this.props.selectedTags, tag]);
+  };
+
+  onUnselect = (tag: string) => {
+    this.props.setProjectTags(without(this.props.selectedTags, tag));
+  };
+
+  render() {
+    const availableTags = difference(this.state.searchResult, this.props.selectedTags);
+    return (
+      <TagsSelector
+        listSize={LIST_SIZE}
+        onSearch={this.onSearch}
+        onSelect={this.onSelect}
+        onUnselect={this.onUnselect}
+        selectedTags={this.props.selectedTags}
+        tags={availableTags}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaKey-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaKey-test.tsx
new file mode 100644 (file)
index 0000000..0fd2885
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import MetaKey, { MetaKeyProps } from '../MetaKey';
+
+it('should render correctly', () => {
+  renderMetaKey();
+  expect(
+    screen.getByLabelText(`overview.project_key.${ComponentQualifier.Project}`)
+  ).toBeInTheDocument();
+  expect(
+    screen.getByRole('button', { name: 'overview.project_key.click_to_copy' })
+  ).toBeInTheDocument();
+});
+
+function renderMetaKey(props: Partial<MetaKeyProps> = {}) {
+  return renderComponent(
+    <MetaKey componentKey="foo" qualifier={ComponentQualifier.Project} {...props} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx
new file mode 100644 (file)
index 0000000..842045d
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { searchRules } from '../../../../api/rules';
+import { mockLanguage, mockPaging, mockQualityProfile } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { SearchRulesResponse } from '../../../../types/coding-rules';
+import { Dict } from '../../../../types/types';
+import { MetaQualityProfiles } from '../MetaQualityProfiles';
+
+jest.mock('../../../../api/rules', () => {
+  return {
+    searchRules: jest.fn().mockResolvedValue({
+      total: 10,
+    }),
+  };
+});
+
+it('should render correctly', async () => {
+  const totals: Dict<number> = {
+    js: 0,
+    ts: 10,
+    css: 0,
+  };
+  jest
+    .mocked(searchRules)
+    .mockImplementation(({ qprofile }: { qprofile: string }): Promise<SearchRulesResponse> => {
+      return Promise.resolve({
+        rules: [],
+        paging: mockPaging({
+          total: totals[qprofile],
+        }),
+      });
+    });
+
+  renderMetaQualityprofiles();
+
+  expect(await screen.findByText('overview.deleted_profile.javascript')).toBeInTheDocument();
+  expect(screen.getByText('overview.deprecated_profile.10')).toBeInTheDocument();
+});
+
+function renderMetaQualityprofiles(overrides: Partial<MetaQualityProfiles['props']> = {}) {
+  return renderComponent(
+    <MetaQualityProfiles
+      languages={{ css: mockLanguage() }}
+      profiles={[
+        { ...mockQualityProfile({ key: 'js', name: 'javascript' }), deleted: true },
+        { ...mockQualityProfile({ key: 'ts', name: 'typescript' }), deleted: false },
+        {
+          ...mockQualityProfile({
+            key: 'css',
+            name: 'style',
+            language: 'css',
+            languageName: 'CSS',
+          }),
+          deleted: false,
+        },
+      ]}
+      {...overrides}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaTags-test.tsx
new file mode 100644 (file)
index 0000000..04b073d
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { searchProjectTags, setApplicationTags, setProjectTags } from '../../../../api/components';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import MetaTags from '../MetaTags';
+
+jest.mock('../../../../api/components', () => ({
+  setApplicationTags: jest.fn().mockResolvedValue(true),
+  setProjectTags: jest.fn().mockResolvedValue(true),
+  searchProjectTags: jest.fn(),
+}));
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+it('should render without tags and admin rights', async () => {
+  renderMetaTags();
+
+  expect(await screen.findByText('no_tags')).toBeInTheDocument();
+  expect(screen.queryByRole('button')).not.toBeInTheDocument();
+});
+
+it('should allow to edit tags for a project', async () => {
+  const user = userEvent.setup();
+  jest.mocked(searchProjectTags).mockResolvedValue({ tags: ['best', 'useless'] });
+
+  const onComponentChange = jest.fn();
+  const component = mockComponent({
+    key: 'my-second-project',
+    tags: ['foo', 'bar'],
+    configuration: {
+      showSettings: true,
+    },
+    name: 'MySecondProject',
+  });
+
+  renderMetaTags({ component, onComponentChange });
+
+  expect(await screen.findByText('foo, bar')).toBeInTheDocument();
+  expect(screen.getByRole('button')).toBeInTheDocument();
+
+  await user.click(screen.getByRole('button', { name: 'tags_list_x.foo, bar' }));
+
+  expect(await screen.findByText('best')).toBeInTheDocument();
+
+  await user.click(screen.getByText('best'));
+  expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo', 'bar', 'best'] });
+
+  onComponentChange.mockClear();
+
+  /*
+   * Since we're not actually updating the tags, we're back to having the foo, bar only
+   */
+  await user.click(screen.getByText('bar'));
+  expect(onComponentChange).toHaveBeenCalledWith({ tags: ['foo'] });
+
+  expect(setProjectTags).toHaveBeenCalled();
+  expect(setApplicationTags).not.toHaveBeenCalled();
+});
+
+it('should set tags for an app', async () => {
+  const user = userEvent.setup();
+
+  renderMetaTags({
+    component: mockComponent({
+      configuration: {
+        showSettings: true,
+      },
+      qualifier: ComponentQualifier.Application,
+    }),
+  });
+
+  await user.click(screen.getByRole('button', { name: 'tags_list_x.no_tags' }));
+
+  await user.click(screen.getByText('best'));
+
+  expect(setProjectTags).not.toHaveBeenCalled();
+  expect(setApplicationTags).toHaveBeenCalled();
+});
+
+function renderMetaTags(overrides: Partial<MetaTags['props']> = {}) {
+  const component = mockComponent({
+    configuration: {
+      showSettings: false,
+    },
+  });
+
+  return renderComponent(
+    <MetaTags component={component} onComponentChange={jest.fn()} {...overrides} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/notifications/ProjectNotifications.tsx b/server/sonar-web/src/main/js/apps/projectInformation/notifications/ProjectNotifications.tsx
new file mode 100644 (file)
index 0000000..d0ddd4f
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 {
+  withNotifications,
+  WithNotificationsProps,
+} from '../../../components/hoc/withNotifications';
+import { Alert } from '../../../components/ui/Alert';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { translate } from '../../../helpers/l10n';
+import { Component } from '../../../types/types';
+import NotificationsList from '../../account/notifications/NotificationsList';
+
+interface Props {
+  component: Component;
+}
+
+export function ProjectNotifications(props: WithNotificationsProps & Props) {
+  const { channels, component, loading, notifications, perProjectTypes } = props;
+  const heading = React.useRef<HTMLHeadingElement>(null);
+
+  React.useEffect(() => {
+    if (heading.current) {
+      // a11y: provide focus to the heading when the info drawer page is opened.
+      heading.current.focus();
+    }
+  }, [heading]);
+
+  const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
+    props.addNotification({ project: component.key, channel, type });
+  };
+
+  const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
+    props.removeNotification({
+      project: component.key,
+      channel,
+      type,
+    });
+  };
+
+  const getCheckboxId = (type: string, channel: string) => {
+    return `project-notification-${component.key}-${type}-${channel}`;
+  };
+
+  const projectNotifications = notifications.filter(
+    (n) => n.project && n.project === component.key
+  );
+
+  return (
+    <div>
+      <h3 tabIndex={-1} ref={heading}>
+        {translate('project.info.notifications')}
+      </h3>
+
+      <Alert className="spacer-top" variant="info">
+        {translate('notification.dispatcher.information')}
+      </Alert>
+
+      <DeferredSpinner loading={loading}>
+        <table className="data zebra notifications-table">
+          <thead>
+            <tr>
+              <th aria-label={translate('project')} />
+              {channels.map((channel) => (
+                <th className="text-center" key={channel}>
+                  <h4>{translate('notification.channel', channel)}</h4>
+                </th>
+              ))}
+            </tr>
+          </thead>
+
+          <NotificationsList
+            channels={channels}
+            checkboxId={getCheckboxId}
+            notifications={projectNotifications}
+            onAdd={handleAddNotification}
+            onRemove={handleRemoveNotification}
+            project
+            types={perProjectTypes}
+          />
+        </table>
+      </DeferredSpinner>
+    </div>
+  );
+}
+
+export default withNotifications(ProjectNotifications);
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx
new file mode 100644 (file)
index 0000000..e7d187c
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { getNotifications } from '../../../../api/notifications';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockNotification } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { NotificationGlobalType, NotificationProjectType } from '../../../../types/notifications';
+import ProjectNotifications from '../ProjectNotifications';
+
+jest.mock('../../../../api/notifications', () => ({
+  addNotification: jest.fn().mockResolvedValue(undefined),
+  removeNotification: jest.fn().mockResolvedValue(undefined),
+  getNotifications: jest.fn(),
+}));
+
+beforeAll(() => {
+  jest.mocked(getNotifications).mockResolvedValue({
+    channels: ['channel1'],
+    globalTypes: [NotificationGlobalType.MyNewIssues],
+    notifications: [
+      mockNotification({}),
+      mockNotification({ type: NotificationProjectType.NewAlerts }),
+    ],
+    perProjectTypes: [NotificationProjectType.NewAlerts, NotificationProjectType.NewIssues],
+  });
+});
+
+it('should render correctly', async () => {
+  const user = userEvent.setup();
+  renderProjectNotifications();
+
+  expect(await screen.findByText('notification.channel.channel1')).toBeInTheDocument();
+  expect(
+    screen.getByLabelText(
+      'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
+    )
+  ).toBeChecked();
+
+  expect(
+    screen.getByLabelText(
+      'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
+    )
+  ).not.toBeChecked();
+
+  // Toggle New Alerts
+  await user.click(
+    screen.getByLabelText(
+      'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
+    )
+  );
+
+  expect(
+    screen.getByLabelText(
+      'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
+    )
+  ).not.toBeChecked();
+
+  // Toggle New Issues
+  await user.click(
+    screen.getByLabelText(
+      'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
+    )
+  );
+
+  expect(
+    screen.getByLabelText(
+      'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project'
+    )
+  ).toBeChecked();
+});
+
+function renderProjectNotifications() {
+  return renderComponent(
+    <ProjectNotifications component={mockComponent({ key: 'foo', name: 'Foo' })} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/RegulatoryReport.tsx b/server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/RegulatoryReport.tsx
new file mode 100644 (file)
index 0000000..59b3f86
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { orderBy } from 'lodash';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { getBranches } from '../../../api/branches';
+import { getRegulatoryReportUrl } from '../../../api/regulatory-report';
+import DocLink from '../../../components/common/DocLink';
+import Select, { LabelValueSelectOption } from '../../../components/controls/Select';
+import { ButtonLink } from '../../../components/controls/buttons';
+import { Alert } from '../../../components/ui/Alert';
+import {
+  getBranchLikeDisplayName,
+  getBranchLikeKey,
+  isMainBranch,
+} from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { Component } from '../../../types/types';
+
+interface Props {
+  component: Pick<Component, 'key' | 'name'>;
+  branchLike?: BranchLike;
+  onClose: () => void;
+}
+
+interface State {
+  downloadStarted: boolean;
+  selectedBranch: string;
+  branchOptions: LabelValueSelectOption[];
+}
+
+export default class RegulatoryReport extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      downloadStarted: false,
+      selectedBranch: '',
+      branchOptions: [],
+    };
+  }
+
+  componentDidMount() {
+    const { component, branchLike } = this.props;
+    getBranches(component.key)
+      .then((data) => {
+        const availableBranches = data.filter(
+          (br) => br.analysisDate && (isMainBranch(br) || br.excludedFromPurge)
+        );
+        const mainBranch = availableBranches.find(isMainBranch);
+        const otherBranchSorted = orderBy(
+          availableBranches.filter((b) => !isMainBranch(b)),
+          (b) => b.name
+        );
+        const sortedBranch = mainBranch ? [mainBranch, ...otherBranchSorted] : otherBranchSorted;
+        const options = sortedBranch.map((br) => {
+          return {
+            value: getBranchLikeDisplayName(br),
+            label: getBranchLikeDisplayName(br),
+          };
+        });
+
+        let selectedBranch = '';
+        if (
+          branchLike &&
+          availableBranches.find((br) => getBranchLikeKey(br) === getBranchLikeKey(branchLike))
+        ) {
+          selectedBranch = getBranchLikeDisplayName(branchLike);
+        } else if (mainBranch) {
+          selectedBranch = getBranchLikeDisplayName(mainBranch);
+        }
+        this.setState({ selectedBranch, branchOptions: options });
+      })
+      .catch(() => {
+        this.setState({ branchOptions: [] });
+      });
+  }
+
+  onBranchSelect = (newOption: LabelValueSelectOption) => {
+    this.setState({ selectedBranch: newOption.value, downloadStarted: false });
+  };
+
+  render() {
+    const { component, onClose } = this.props;
+    const { downloadStarted, selectedBranch, branchOptions } = this.state;
+    const isDownloadButtonDisabled = downloadStarted || !selectedBranch;
+
+    return (
+      <>
+        <div className="modal-head">
+          <h2>{translate('regulatory_report.page')}</h2>
+        </div>
+        <div className="modal-body">
+          <p>{translate('regulatory_report.description1')}</p>
+          <div className="markdown">
+            <ul>
+              <li>{translate('regulatory_report.bullet_point1')}</li>
+              <li>{translate('regulatory_report.bullet_point2')}</li>
+              <li>{translate('regulatory_report.bullet_point3')}</li>
+            </ul>
+          </div>
+          <p>{translate('regulatory_report.description2')}</p>
+          {branchOptions.length > 0 ? (
+            <>
+              <div className="modal-field big-spacer-top">
+                <label htmlFor="regulatory-report-branch-select">
+                  {translate('regulatory_page.select_branch')}
+                </label>
+                <Select
+                  className="width-100"
+                  inputId="regulatory-report-branch-select"
+                  id="regulatory-report-branch-select-input"
+                  onChange={this.onBranchSelect}
+                  options={branchOptions}
+                  value={branchOptions.find((o) => o.value === selectedBranch)}
+                />
+              </div>
+              <Alert variant="info">
+                <div>
+                  {translate('regulatory_page.available_branches_info.only_keep_when_inactive')}
+                </div>
+                <div>
+                  <FormattedMessage
+                    id="regulatory_page.available_branches_info.more_info"
+                    defaultMessage={translate('regulatory_page.available_branches_info.more_info')}
+                    values={{
+                      doc_link: (
+                        <DocLink to="/analyzing-source-code/branches/branch-analysis/#inactive-branches">
+                          {translate('regulatory_page.available_branches_info.more_info.doc_link')}
+                        </DocLink>
+                      ),
+                    }}
+                  />
+                </div>
+              </Alert>
+            </>
+          ) : (
+            <div className="big-spacer-top">
+              <Alert variant="warning">
+                <div>{translate('regulatory_page.no_available_branch')}</div>
+              </Alert>
+            </div>
+          )}
+          <div className="modal-field big-spacer-top">
+            {downloadStarted && (
+              <div>
+                <p>{translate('regulatory_page.download_start.sentence')}</p>
+              </div>
+            )}
+          </div>
+        </div>
+        <div className="modal-foot">
+          <a
+            className={classNames('button button-primary big-spacer-right', {
+              disabled: isDownloadButtonDisabled,
+            })}
+            download={[component.name, selectedBranch, 'regulatory report.zip']
+              .filter((s) => !!s)
+              .join(' - ')}
+            onClick={() => this.setState({ downloadStarted: true })}
+            href={getRegulatoryReportUrl(component.key, selectedBranch)}
+            target="_blank"
+            rel="noopener noreferrer"
+            aria-disabled={isDownloadButtonDisabled}
+          >
+            {translate('download_verb')}
+          </a>
+          <ButtonLink onClick={onClose}>{translate('close')}</ButtonLink>
+        </div>
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/RegulatoryReportModal.tsx b/server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/RegulatoryReportModal.tsx
new file mode 100644 (file)
index 0000000..8311b93
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 ClickEventBoundary from '../../../components/controls/ClickEventBoundary';
+import Modal from '../../../components/controls/Modal';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { Component } from '../../../types/types';
+import RegulatoryReport from './RegulatoryReport';
+
+interface Props {
+  component: Component;
+  branchLike?: BranchLike;
+  onClose: () => void;
+}
+
+export default function RegulatoryReportModal(props: Props) {
+  const { component, branchLike } = props;
+  return (
+    <Modal contentLabel={translate('regulatory_report.page')} onRequestClose={props.onClose}>
+      <ClickEventBoundary>
+        <form>
+          <RegulatoryReport component={component} branchLike={branchLike} onClose={props.onClose} />
+        </form>
+      </ClickEventBoundary>
+    </Modal>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/__tests__/RegulatoryReport-it.tsx b/server/sonar-web/src/main/js/apps/projectInformation/projectRegulatoryReport/__tests__/RegulatoryReport-it.tsx
new file mode 100644 (file)
index 0000000..2d2a2f2
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { BranchLike } from '../../../../types/branch-like';
+import RegulatoryReport from '../RegulatoryReport';
+
+let handler: BranchesServiceMock;
+
+beforeAll(() => {
+  handler = new BranchesServiceMock();
+});
+
+afterEach(() => handler.reset());
+
+describe('RegulatoryReport tests', () => {
+  it('should open the regulatory report page', async () => {
+    const user = userEvent.setup();
+    renderRegulatoryReportApp();
+    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+    expect(screen.getByText('regulatory_report.description1')).toBeInTheDocument();
+    expect(screen.getByText('regulatory_report.description2')).toBeInTheDocument();
+    expect(
+      screen.getByText('regulatory_page.available_branches_info.only_keep_when_inactive')
+    ).toBeInTheDocument();
+    expect(
+      screen.getByText('regulatory_page.available_branches_info.more_info')
+    ).toBeInTheDocument();
+
+    const branchSelect = screen.getByRole('combobox', { name: 'regulatory_page.select_branch' });
+    expect(branchSelect).toBeInTheDocument();
+
+    await user.click(branchSelect);
+    await user.keyboard('[ArrowDown][Enter]');
+
+    const downloadButton = screen.getByRole('link', { name: 'download_verb' });
+    expect(downloadButton).toBeInTheDocument();
+
+    expect(screen.queryByText('regulatory_page.download_start.sentence')).not.toBeInTheDocument();
+    await user.click(downloadButton);
+    expect(screen.getByText('regulatory_page.download_start.sentence')).toBeInTheDocument();
+  });
+
+  it('should display warning message if there is no available branch', async () => {
+    handler.emptyBranches();
+    renderRegulatoryReportApp();
+
+    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+    expect(screen.getByText('regulatory_page.no_available_branch')).toBeInTheDocument();
+
+    const downloadButton = screen.getByRole('link', { name: 'download_verb' });
+    expect(downloadButton).toBeInTheDocument();
+    expect(downloadButton).toHaveClass('disabled');
+  });
+
+  it('should automatically select passed branch if compatible', async () => {
+    const compatibleBranch = mockBranch({ name: 'compatible-branch' });
+    handler.addBranch(compatibleBranch);
+    renderRegulatoryReportApp(compatibleBranch);
+
+    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+    const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
+    expect(downloadButton).toBeInTheDocument();
+    expect(downloadButton).not.toHaveClass('disabled');
+    expect(downloadButton.href).toContain(compatibleBranch.name);
+  });
+
+  it('should automatically select main branch if present and passed branch is not compatible', async () => {
+    handler.emptyBranches();
+    const mainBranch = mockMainBranch({ name: 'main' });
+    const notCompatibleBranch = mockBranch({
+      name: 'not-compatible-branch',
+      excludedFromPurge: false,
+    });
+    handler.addBranch(mainBranch);
+    handler.addBranch(notCompatibleBranch);
+    renderRegulatoryReportApp(notCompatibleBranch);
+
+    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+    const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
+    expect(downloadButton).toBeInTheDocument();
+    expect(downloadButton).not.toHaveClass('disabled');
+    expect(downloadButton.href).toContain(mainBranch.name);
+  });
+});
+
+function renderRegulatoryReportApp(branchLike?: BranchLike) {
+  renderComponent(
+    <RegulatoryReport
+      component={{ key: '', name: '' }}
+      branchLike={branchLike}
+      onClose={() => {}}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/routes.tsx b/server/sonar-web/src/main/js/apps/projectInformation/routes.tsx
new file mode 100644 (file)
index 0000000..669de90
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { Route } from 'react-router-dom';
+import ProjectInformationApp from './ProjectInformationApp';
+
+const routes = () => <Route path="project/info" element={<ProjectInformationApp />} />;
+
+export default routes;