]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19613 Migrate About this project to MIUI
authorViktor Vorona <viktor.vorona@sonarsource.com>
Fri, 23 Jun 2023 14:22:45 +0000 (16:22 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Jun 2023 20:03:55 +0000 (20:03 +0000)
33 files changed:
server/sonar-web/design-system/src/components/SizeIndicator.tsx
server/sonar-web/design-system/src/components/__tests__/SizeIndicator-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx
server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaHome.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaKey-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaTags-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaKey.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLink.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLinks.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityGate.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityProfiles.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaSize.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTags.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTagsSelector.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaKey-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaTags-test.tsx [deleted file]
server/sonar-web/src/main/js/components/common/MetaLink.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons/ProjectLinkIcon.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 62cc685c2e76cc827a771ba7e483e1095a48dce0..a5a4d54259b26d25d17f46c682bd77bf4ab6692c 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import styled from '@emotion/styled';
+import { inRange } from 'lodash';
 import tw from 'twin.macro';
 import { getProp, themeColor, themeContrast } from '../helpers/theme';
 import { SizeLabel } from '../types/measures';
 
 export interface Props {
   size?: 'xs' | 'sm' | 'md';
-  value: SizeLabel;
+  value: number;
 }
 
 const SIZE_MAPPING = {
@@ -34,9 +35,22 @@ const SIZE_MAPPING = {
 };
 
 export function SizeIndicator({ size = 'sm', value }: Props) {
+  let letter: SizeLabel;
+  if (inRange(value, 0, 1000)) {
+    letter = 'XS';
+  } else if (inRange(value, 1000, 10000)) {
+    letter = 'S';
+  } else if (inRange(value, 10000, 100000)) {
+    letter = 'M';
+  } else if (inRange(value, 100000, 500000)) {
+    letter = 'L';
+  } else {
+    letter = 'XL';
+  }
+
   return (
     <StyledContainer aria-hidden="true" size={SIZE_MAPPING[size]}>
-      {value}
+      {letter}
     </StyledContainer>
   );
 }
@@ -44,7 +58,7 @@ export function SizeIndicator({ size = 'sm', value }: Props) {
 const StyledContainer = styled.div<{ size: string }>`
   width: ${getProp('size')};
   height: ${getProp('size')};
-  font-size: ${({ size }) => (size === '2rem' ? '0.875rem' : '0.75rem')};
+  font-size: ${({ size }) => (size === '2rem' ? '0.875rem' : `calc(${size}/2)`)};
   color: ${themeContrast('sizeIndicator')};
   background-color: ${themeColor('sizeIndicator')};
 
index bdd8b8c6d855679ec593d3a23b508feeea13476c..908197ec19a4e7119c346d42adc62a631bc5b26e 100644 (file)
@@ -25,14 +25,17 @@ import { FCProps } from '../../types/misc';
 import { SizeLabel } from '../../types/measures';
 import { SizeIndicator } from '../SizeIndicator';
 
-it.each(['XS', 'S', 'M', 'L', 'XL'])(
-  'should display SizeIndicator with size',
-  (value: SizeLabel) => {
-    setupWithProps({ value });
-    expect(screen.getByText(value)).toBeInTheDocument();
-  }
-);
+it.each([
+  [100, 'XS'],
+  [1100, 'S'],
+  [20_000, 'M'],
+  [200_000, 'L'],
+  [1_000_000, 'XL'],
+])('should display SizeIndicator with size', (value: number, letter: SizeLabel) => {
+  setupWithProps({ value });
+  expect(screen.getByText(letter)).toBeInTheDocument();
+});
 
 function setupWithProps(props: Partial<FCProps<typeof SizeIndicator>> = {}) {
-  return render(<SizeIndicator value="XS" {...props} />);
+  return render(<SizeIndicator value={0} {...props} />);
 }
index 35ad1906cb9cd0864955dc1c9f0c7b80d8cf901c..57bd0ba3d699d09c5fa20f1914d992ff8baadb36 100644 (file)
@@ -67,7 +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.getByText('/project/info?id=my-project')).toBeInTheDocument();
+  expect(screen.getByText('/project/information?id=my-project')).toBeInTheDocument();
 });
 
 function renderComponentNav(props: Partial<ComponentNavProps> = {}) {
index 93899de2a3fb07175e58b858e688a1d2c86ea99d..dc29d25eaa25792b9027781b5adbe64192dbbb5a 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import Link from '../../../components/common/Link';
+import MetaLink from '../../../components/common/MetaLink';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import Level from '../../../components/ui/Level';
@@ -26,7 +27,6 @@ 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;
index 765e19b795da05993bec67ccc73d2dc13949fc6b..223c019c707ed5bf9948056a070ba1dd966d44fa 100644 (file)
@@ -28,7 +28,7 @@ import withCurrentUserContext from '../../app/components/current-user/withCurren
 import withMetricsContext from '../../app/components/metrics/withMetricsContext';
 import { translate } from '../../helpers/l10n';
 import { BranchLike } from '../../types/branch-like';
-import { ComponentQualifier } from '../../types/component';
+import { ComponentQualifier, isApplication, isProject } from '../../types/component';
 import { Feature } from '../../types/features';
 import { MetricKey } from '../../types/metrics';
 import { Component, Dict, Measure, Metric } from '../../types/types';
@@ -82,14 +82,12 @@ export class ProjectInformationApp extends React.PureComponent<Props, State> {
     const { branchLike, component, currentUser, metrics } = this.props;
     const { measures } = this.state;
 
-    const canConfigureNotifications =
-      isLoggedIn(currentUser) && component.qualifier === ComponentQualifier.Project;
+    const canConfigureNotifications = isLoggedIn(currentUser) && isProject(component.qualifier);
     const canUseBadges =
       metrics !== undefined &&
-      (component.qualifier === ComponentQualifier.Application ||
-        component.qualifier === ComponentQualifier.Project);
+      (isApplication(component.qualifier) || isProject(component.qualifier));
     const regulatoryReportFeatureEnabled = this.props.hasFeature(Feature.RegulatoryReport);
-    const isApp = component.qualifier === ComponentQualifier.Application;
+    const isApp = isApplication(component.qualifier);
 
     return (
       <main>
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx b/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx
new file mode 100644 (file)
index 0000000..cebd49e
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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 ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
+import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../helpers/testSelector';
+import routes from '../routes';
+
+const componentsMock = new ComponentsServiceMock();
+
+const ui = {
+  projectPageTitle: byRole('heading', { name: 'project.info.title' }),
+  applicationPageTitle: byRole('heading', { name: 'application.info.title' }),
+  qualityGateList: byRole('list', { name: 'project.info.quality_gate' }),
+  qualityProfilesList: byRole('list', { name: 'project.info.qualit_profiles' }),
+  link: byRole('link'),
+  tags: byRole('generic', { name: /tags:/ }),
+  size: byRole('link', { name: /project.info.see_more_info_on_x_locs/ }),
+  newKeyInput: byRole('textbox'),
+  updateInputButton: byRole('button', { name: 'update_verb' }),
+  resetInputButton: byRole('button', { name: 'reset_verb' }),
+};
+
+afterEach(() => {
+  componentsMock.reset();
+});
+
+it('can update project key', async () => {
+  renderProjectInformationApp();
+  expect(await ui.projectPageTitle.find()).toBeInTheDocument();
+});
+
+function renderProjectInformationApp() {
+  return renderAppWithComponentContext(
+    'project/information',
+    routes,
+    {},
+    { component: componentsMock.components[0].component }
+  );
+}
index 5b2b9a38d50ad31b1430fdd9d0f508fa3a4a47b1..09dd1ec8bed3c76ecec793bf1e924d4f89a1cafb 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import * as React from 'react';
-import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';
+import classNames from 'classnames';
+import { BasicSeparator, SubTitle } from 'design-system';
+import React, { PropsWithChildren, useContext, useEffect, useState } from 'react';
+import { getProjectLinks } from '../../../api/projectLinks';
+import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
 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';
+import { ComponentQualifier, Visibility } from '../../../types/component';
+import { Component, Measure, ProjectLink } from '../../../types/types';
+import { isLoggedIn } from '../../../types/users';
+import MetaDescription from './components/MetaDescription';
+import MetaHome from './components/MetaHome';
+import MetaKey from './components/MetaKey';
+import MetaLinks from './components/MetaLinks';
+import MetaQualityGate from './components/MetaQualityGate';
+import MetaQualityProfiles from './components/MetaQualityProfiles';
+import MetaSize from './components/MetaSize';
+import MetaTags from './components/MetaTags';
+import MetaVisibility from './components/MetaVisibility';
 
 export interface AboutProjectProps {
   component: Component;
@@ -35,73 +42,90 @@ export interface AboutProjectProps {
   onComponentChange: (changes: {}) => void;
 }
 
-export function AboutProject(props: AboutProjectProps) {
+export default function AboutProject(props: AboutProjectProps) {
+  const { currentUser } = useContext(CurrentUserContext);
   const { component, measures = [] } = props;
-
-  const heading = React.useRef<HTMLHeadingElement>(null);
   const isApp = component.qualifier === ComponentQualifier.Application;
+  const [links, setLinks] = useState<ProjectLink[] | undefined>(undefined);
 
-  React.useEffect(() => {
-    if (heading.current) {
-      // a11y: provide focus to the heading when the Project Information is opened.
-      heading.current.focus();
+  useEffect(() => {
+    if (!isApp) {
+      getProjectLinks(component.key).then(
+        (links) => setLinks(links),
+        () => {}
+      );
     }
-  }, [heading]);
+  }, [component.key, isApp]);
 
   return (
     <>
       <div>
-        <h2 className="big-padded bordered-bottom" tabIndex={-1} ref={heading}>
-          {translate(isApp ? 'application' : 'project', 'info.title')}
-        </h2>
+        <SubTitle>{translate(isApp ? 'application' : 'project', 'about.title')}</SubTitle>
       </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}
+      {!isApp &&
+        (component.qualityGate ||
+          (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
+          <ProjectInformationSection className="sw-pt-0">
+            {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
+
+            {component.qualityProfiles && component.qualityProfiles.length > 0 && (
+              <MetaQualityProfiles
+                headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
+                profiles={component.qualityProfiles}
               />
             )}
-          </div>
+          </ProjectInformationSection>
+        )}
 
-          {component.description && (
-            <p className="it__project-description">{component.description}</p>
-          )}
+      <ProjectInformationSection>
+        <MetaKey componentKey={component.key} qualifier={component.qualifier} />
+      </ProjectInformationSection>
 
-          <MetaTags component={component} onComponentChange={props.onComponentChange} />
-        </div>
+      {component.visibility === Visibility.Private && (
+        <ProjectInformationSection>
+          <MetaVisibility qualifier={component.qualifier} visibility={component.visibility} />
+        </ProjectInformationSection>
+      )}
 
-        <div className="big-padded bordered-bottom it__project-loc-value">
-          <MetaSize component={component} measures={measures} />
-        </div>
+      <ProjectInformationSection>
+        <MetaDescription description={component.description} isApp={isApp} />
+      </ProjectInformationSection>
 
-        {!isApp &&
-          (component.qualityGate ||
-            (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
-            <div className="big-padded bordered-bottom">
-              {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
+      <ProjectInformationSection>
+        <MetaTags component={component} onComponentChange={props.onComponentChange} />
+      </ProjectInformationSection>
 
-              {component.qualityProfiles && component.qualityProfiles.length > 0 && (
-                <MetaQualityProfiles
-                  headerClassName={component.qualityGate ? 'big-spacer-top' : undefined}
-                  profiles={component.qualityProfiles}
-                />
-              )}
-            </div>
-          )}
+      <ProjectInformationSection>
+        <MetaSize component={component} measures={measures} />
+      </ProjectInformationSection>
 
-        {!isApp && <MetaLinks component={component} />}
+      {!isApp && links && links.length > 0 && (
+        <ProjectInformationSection last={!isLoggedIn(currentUser)}>
+          <MetaLinks links={links} />
+        </ProjectInformationSection>
+      )}
 
-        <div className="big-padded bordered-bottom">
-          <MetaKey componentKey={component.key} qualifier={component.qualifier} />
-        </div>
-      </div>
+      {isLoggedIn(currentUser) && (
+        <ProjectInformationSection last>
+          <MetaHome componentKey={component.key} currentUser={currentUser} isApp={isApp} />
+        </ProjectInformationSection>
+      )}
     </>
   );
 }
 
-export default AboutProject;
+interface ProjectInformationSectionProps {
+  last?: boolean;
+  className?: string;
+}
+
+function ProjectInformationSection(props: PropsWithChildren<ProjectInformationSectionProps>) {
+  const { children, className, last = false } = props;
+  return (
+    <>
+      <div className={classNames('sw-py-6', className)}>{children}</div>
+      {!last && <BasicSeparator />}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx
new file mode 100644 (file)
index 0000000..9a9e3e5
--- /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 { TextMuted } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  description?: string;
+  isApp?: boolean;
+}
+
+export default function MetaDescription({ description, isApp }: Props) {
+  return (
+    <>
+      <h3>{translate('project.info.description')}</h3>
+      {description ? (
+        <p className="it__project-description">{description}</p>
+      ) : (
+        <TextMuted text={translate(isApp ? 'application' : 'project', 'info.empty_description')} />
+      )}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaHome.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaHome.tsx
new file mode 100644 (file)
index 0000000..cf79ab1
--- /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 { Checkbox, HelperHintIcon } from 'design-system';
+import React, { useContext } from 'react';
+import { setHomePage } from '../../../../api/users';
+import { CurrentUserContext } from '../../../../app/components/current-user/CurrentUserContext';
+import HelpTooltip from '../../../../components/controls/HelpTooltip';
+import { DEFAULT_HOMEPAGE } from '../../../../components/controls/HomePageSelect';
+import { translate } from '../../../../helpers/l10n';
+import { isSameHomePage } from '../../../../helpers/users';
+import { HomePage, LoggedInUser, isLoggedIn } from '../../../../types/users';
+
+export interface MetaHomeProps {
+  componentKey: string;
+  currentUser: LoggedInUser;
+  isApp?: boolean;
+}
+
+export default function MetaHome({ componentKey, currentUser, isApp }: MetaHomeProps) {
+  const { updateCurrentUserHomepage } = useContext(CurrentUserContext);
+  const currentPage: HomePage = {
+    component: componentKey,
+    type: 'PROJECT',
+    branch: undefined,
+  };
+
+  const setCurrentUserHomepage = async (homepage: HomePage) => {
+    if (isLoggedIn(currentUser)) {
+      await setHomePage(homepage);
+
+      updateCurrentUserHomepage(homepage);
+    }
+  };
+
+  const handleClick = (value: boolean) => {
+    setCurrentUserHomepage(value ? currentPage : DEFAULT_HOMEPAGE);
+  };
+
+  return (
+    <>
+      <div className="sw-flex sw-items-center">
+        <h3>{translate('project.info.make_home.title')}</h3>
+        <HelpTooltip
+          className="sw-ml-1"
+          overlay={
+            <p className="sw-max-w-abs-250">
+              {translate(isApp ? 'application' : 'project', 'info.make_home.tooltip')}
+            </p>
+          }
+        >
+          <HelperHintIcon />
+        </HelpTooltip>
+      </div>
+      <Checkbox
+        checked={
+          currentUser.homepage !== undefined && isSameHomePage(currentUser.homepage, currentPage)
+        }
+        onCheck={handleClick}
+      >
+        <span className="sw-ml-2">
+          {translate(isApp ? 'application' : 'project', 'info.make_home.label')}
+        </span>
+      </Checkbox>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx
new file mode 100644 (file)
index 0000000..f5b023c
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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 { ClipboardIconButton, CodeSnippet, HelperHintIcon } from 'design-system';
+import * as React from 'react';
+import HelpTooltip from '../../../../components/controls/HelpTooltip';
+import { translate } from '../../../../helpers/l10n';
+
+interface MetaKeyProps {
+  componentKey: string;
+  qualifier: string;
+}
+
+export default function MetaKey({ componentKey, qualifier }: MetaKeyProps) {
+  return (
+    <>
+      <div className="sw-flex sw-items-center">
+        <h3>{translate('overview.project_key', qualifier)}</h3>
+        <HelpTooltip
+          className="sw-ml-1"
+          overlay={
+            <p className="sw-max-w-abs-250">
+              {translate('overview.project_key.tooltip', qualifier)}
+            </p>
+          }
+        >
+          <HelperHintIcon />
+        </HelpTooltip>
+      </div>
+      <div className="sw-w-full">
+        <div className="sw-flex sw-gap-2 sw-items-center sw-min-w-0">
+          <CodeSnippet
+            className="sw-min-w-0"
+            isOneLine
+            noCopy
+            highlight={false}
+            snippet={componentKey}
+          />
+          <ClipboardIconButton copyValue={componentKey} />
+        </div>
+      </div>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx
new file mode 100644 (file)
index 0000000..15cc747
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 MetaLink from '../../../../components/common/MetaLink';
+import { translate } from '../../../../helpers/l10n';
+import { orderLinks } from '../../../../helpers/projectLinks';
+import { ProjectLink } from '../../../../types/types';
+
+interface Props {
+  links: ProjectLink[];
+}
+
+export default function MetaLinks({ links }: Props) {
+  const orderedLinks = orderLinks(links);
+
+  return (
+    <>
+      <h3>{translate('overview.external_links')}</h3>
+      <ul className="project-info-list">
+        {orderedLinks.map((link) => (
+          <MetaLink miui key={link.id} link={link} />
+        ))}
+      </ul>
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx
new file mode 100644 (file)
index 0000000..745aa85
--- /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 { Link } from 'design-system';
+import * as React from 'react';
+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 id="quality-gate-header">{translate('project.info.quality_gate')}</h3>
+
+      <ul className="project-info-list" aria-labelledby="quality-gate-header">
+        <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/about/components/MetaQualityProfiles.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx
new file mode 100644 (file)
index 0000000..87d7b04
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * 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 { Badge, Link } from 'design-system';
+import React, { useContext, useEffect } from 'react';
+import { searchRules } from '../../../../api/rules';
+import { LanguagesContext } from '../../../../app/components/languages/LanguagesContext';
+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;
+  profiles: ComponentQualityProfile[];
+}
+
+export function MetaQualityProfiles({ headerClassName, profiles }: Props) {
+  const [deprecatedByKey, setDeprecatedByKey] = React.useState<Dict<number>>({});
+  const languages = useContext(LanguagesContext);
+
+  useEffect(() => {
+    const existingProfiles = profiles.filter((p) => !p.deleted);
+    const requests = existingProfiles.map((profile) => {
+      const data = {
+        activation: 'true',
+        ps: 1,
+        qprofile: profile.key,
+        statuses: 'DEPRECATED',
+      };
+      return searchRules(data).then((r) => r.paging.total);
+    });
+    Promise.all(requests).then(
+      (responses) => {
+        const deprecatedByKey: Dict<number> = {};
+        responses.forEach((count, i) => {
+          const profileKey = existingProfiles[i].key;
+          deprecatedByKey[profileKey] = count;
+        });
+        setDeprecatedByKey(deprecatedByKey);
+      },
+      () => {}
+    );
+  }, [profiles]);
+
+  return (
+    <>
+      <h3 className={headerClassName} id="quality-profiles-list">
+        {translate('overview.quality_profiles')}
+      </h3>
+      <ul className="project-info-list" aria-labelledby="quality-profiles-list">
+        {profiles.map((profile) => (
+          <ProfileItem
+            key={profile.key}
+            profile={profile}
+            languages={languages}
+            deprecatedByKey={deprecatedByKey}
+          />
+        ))}
+      </ul>
+    </>
+  );
+}
+
+function ProfileItem({
+  profile,
+  languages,
+  deprecatedByKey,
+}: {
+  profile: ComponentQualityProfile;
+  languages: Languages;
+  deprecatedByKey: Dict<number>;
+}) {
+  const languageFromStore = languages[profile.language];
+  const languageName = languageFromStore ? languageFromStore.name : profile.language;
+  const count = deprecatedByKey[profile.key] || 0;
+
+  return (
+    <li>
+      <div className="sw-grid sw-grid-cols-[1fr_3fr]">
+        <span>{languageName}</span>
+        <div>
+          {profile.deleted ? (
+            <Tooltip
+              key={profile.key}
+              overlay={translateWithParameters('overview.deleted_profile', profile.name)}
+            >
+              <div className="project-info-deleted-profile">{profile.name}</div>
+            </Tooltip>
+          ) : (
+            <>
+              <Link to={getQualityProfileUrl(profile.name, profile.language)}>
+                <span
+                  aria-label={translateWithParameters(
+                    'overview.link_to_x_profile_y',
+                    languageName,
+                    profile.name
+                  )}
+                >
+                  {profile.name}
+                </span>
+              </Link>
+              {count > 0 && (
+                <Tooltip
+                  key={profile.key}
+                  overlay={translateWithParameters('overview.deprecated_profile', count)}
+                >
+                  <span>
+                    <Badge className="sw-ml-6" variant="deleted">
+                      {translate('deprecated')}
+                    </Badge>
+                  </span>
+                </Tooltip>
+              )}
+            </>
+          )}
+        </div>
+      </div>
+    </li>
+  );
+}
+
+export default MetaQualityProfiles;
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx
new file mode 100644 (file)
index 0000000..a015f04
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * 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 { DrilldownLink, SizeIndicator } from 'design-system';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { formatMeasure, localizeMetric } from '../../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../../helpers/urls';
+import { ComponentQualifier } from '../../../../types/component';
+import { MetricKey } from '../../../../types/metrics';
+import { Component, Measure } from '../../../../types/types';
+
+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;
+  const url = getComponentDrilldownUrl({
+    componentKey: component.key,
+    metric: MetricKey.ncloc,
+    listView: true,
+  });
+
+  return (
+    <>
+      <div className="sw-flex sw-items-center">
+        <h3>{localizeMetric(MetricKey.ncloc)}</h3>
+        <span className="sw-ml-1 small">({translate('project.info.main_branch')})</span>
+      </div>
+      <div className="sw-flex sw-items-center">
+        {ncloc && ncloc.value ? (
+          <>
+            <DrilldownLink className="huge" to={url}>
+              <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">
+              <SizeIndicator value={Number(ncloc.value)} size="xs" />
+            </span>
+          </>
+        ) : (
+          <span>0</span>
+        )}
+
+        {isApp && (
+          <span className="huge-spacer-left display-inline-flex-center">
+            {projects ? (
+              <DrilldownLink to={url}>
+                <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/about/components/MetaTags.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx
new file mode 100644 (file)
index 0000000..68abe43
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * 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 { Tags, TagsSelector } from 'design-system';
+import { difference, without } from 'lodash';
+import React, { useState } from 'react';
+import { searchProjectTags, setApplicationTags, setProjectTags } from '../../../../api/components';
+import Tooltip from '../../../../components/controls/Tooltip';
+import { PopupPlacement } from '../../../../components/ui/popups';
+import { translate } from '../../../../helpers/l10n';
+import { ComponentQualifier } from '../../../../types/component';
+import { Component } from '../../../../types/types';
+
+interface Props {
+  component: Component;
+  onComponentChange: (changes: {}) => void;
+}
+
+export default function MetaTags(props: Props) {
+  const [open, setOpen] = React.useState(false);
+
+  const canUpdateTags = () => {
+    const { configuration } = props.component;
+    return configuration && configuration.showSettings;
+  };
+
+  const setTags = (values: string[]) => {
+    const { component } = props;
+
+    if (component.qualifier === ComponentQualifier.Application) {
+      return setApplicationTags({
+        application: component.key,
+        tags: values.join(','),
+      });
+    }
+
+    return setProjectTags({
+      project: component.key,
+      tags: values.join(','),
+    });
+  };
+
+  const handleSetProjectTags = (values: string[]) => {
+    setTags(values).then(
+      () => props.onComponentChange({ tags: values }),
+      () => {}
+    );
+  };
+
+  const tags = props.component.tags || [];
+
+  return (
+    <>
+      <h3>{translate('tags')}</h3>
+      <Tags
+        allowUpdate={canUpdateTags()}
+        ariaTagsListLabel={translate('tags')}
+        className="js-issue-edit-tags"
+        emptyText={translate('no_tags')}
+        overlay={<MetaTagsSelector selectedTags={tags} setProjectTags={handleSetProjectTags} />}
+        popupPlacement={PopupPlacement.Bottom}
+        tags={tags}
+        tagsToDisplay={2}
+        tooltip={Tooltip}
+        open={open}
+        onClose={() => setOpen(false)}
+      />
+    </>
+  );
+}
+
+interface MetaTagsSelectorProps {
+  selectedTags: string[];
+  setProjectTags: (tags: string[]) => void;
+}
+
+const LIST_SIZE = 10;
+
+function MetaTagsSelector({ selectedTags, setProjectTags }: MetaTagsSelectorProps) {
+  const [searchResult, setSearchResult] = useState<string[]>([]);
+  const availableTags = difference(searchResult, selectedTags);
+
+  const onSearch = (query: string) => {
+    return searchProjectTags({
+      q: query,
+      ps: Math.min(selectedTags.length - 1 + LIST_SIZE, 100),
+    }).then(
+      ({ tags }) => setSearchResult(tags),
+      () => {}
+    );
+  };
+
+  const onSelect = (tag: string) => {
+    setProjectTags([...selectedTags, tag]);
+  };
+
+  const onUnselect = (tag: string) => {
+    setProjectTags(without(selectedTags, tag));
+  };
+
+  return (
+    <TagsSelector
+      headerLabel={translate('tags')}
+      searchInputAriaLabel={translate('search.search_for_tags')}
+      clearIconAriaLabel={translate('clear')}
+      createElementLabel={translate('issue.create_tag')}
+      noResultsLabel={translate('no_results')}
+      onSearch={onSearch}
+      onSelect={onSelect}
+      onUnselect={onUnselect}
+      selectedTags={selectedTags}
+      tags={availableTags}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx
new file mode 100644 (file)
index 0000000..c5fdb3d
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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 { Visibility } from '../../../../types/component';
+
+interface Props {
+  qualifier: string;
+  visibility: Visibility;
+}
+
+export default function MetaVisibility({ qualifier, visibility }: Props) {
+  return (
+    <>
+      <h3>{translate('visibility')}</h3>
+      <PrivacyBadgeContainer qualifier={qualifier} visibility={visibility} />
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaKey-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaKey-test.tsx
new file mode 100644 (file)
index 0000000..17d2a8c
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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 from '../MetaKey';
+
+it('should render correctly', () => {
+  renderMetaKey();
+  expect(
+    screen.getByText(`overview.project_key.${ComponentQualifier.Project}`)
+  ).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument();
+});
+
+function renderMetaKey(props: Partial<Parameters<typeof MetaKey>[0]> = {}) {
+  return renderComponent(
+    <MetaKey componentKey="foo" qualifier={ComponentQualifier.Project} {...props} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx
new file mode 100644 (file)
index 0000000..8d2f51f
--- /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 { screen } from '@testing-library/react';
+import * as React from 'react';
+import { searchRules } from '../../../../../api/rules';
+import { LanguagesContext } from '../../../../../app/components/languages/LanguagesContext';
+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<Parameters<typeof MetaQualityProfiles>[0]> = {}
+) {
+  return renderComponent(
+    <LanguagesContext.Provider value={{ css: mockLanguage() }}>
+      <MetaQualityProfiles
+        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}
+      />
+    </LanguagesContext.Provider>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaTags-test.tsx
new file mode 100644 (file)
index 0000000..88511e5
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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 { act, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { 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().mockResolvedValue({ tags: ['best', 'useless'] }),
+}));
+
+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();
+
+  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 act(() => user.click(screen.getByRole('button', { name: 'foo, bar +' })));
+
+  expect(await screen.findByRole('checkbox', { name: 'best' })).toBeInTheDocument();
+
+  await user.click(screen.getByRole('checkbox', { name: '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.getByRole('checkbox', { name: '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 act(() => user.click(screen.getByRole('button', { name: 'no_tags +' })));
+
+  await user.click(await screen.findByRole('checkbox', { name: 'best' }));
+
+  expect(setProjectTags).not.toHaveBeenCalled();
+  expect(setApplicationTags).toHaveBeenCalled();
+});
+
+function renderMetaTags(overrides: Partial<Parameters<typeof MetaTags>[0]> = {}) {
+  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/meta/MetaKey.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaKey.tsx
deleted file mode 100644 (file)
index 8532fac..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/apps/projectInformation/meta/MetaLink.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaLink.tsx
deleted file mode 100644 (file)
index e81f53c..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 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
deleted file mode 100644 (file)
index c6df449..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/apps/projectInformation/meta/MetaQualityGate.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityGate.tsx
deleted file mode 100644 (file)
index 94719fc..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/apps/projectInformation/meta/MetaQualityProfiles.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaQualityProfiles.tsx
deleted file mode 100644 (file)
index 1788efd..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 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
deleted file mode 100644 (file)
index 499d3b1..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/apps/projectInformation/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/MetaTags.tsx
deleted file mode 100644 (file)
index d12a0bf..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 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
deleted file mode 100644 (file)
index d2602d4..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/apps/projectInformation/meta/__tests__/MetaKey-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaKey-test.tsx
deleted file mode 100644 (file)
index 0fd2885..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/apps/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaQualityProfiles-test.tsx
deleted file mode 100644 (file)
index 842045d..0000000
+++ /dev/null
@@ -1,80 +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/apps/projectInformation/meta/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/meta/__tests__/MetaTags-test.tsx
deleted file mode 100644 (file)
index 04b073d..0000000
+++ /dev/null
@@ -1,114 +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/components/common/MetaLink.tsx b/server/sonar-web/src/main/js/components/common/MetaLink.tsx
new file mode 100644 (file)
index 0000000..c2d73bd
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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 { CloseIcon, InputField, InteractiveIcon, Link } from 'design-system';
+import React, { useState } from 'react';
+import isValidUri from '../../app/utils/isValidUri';
+import { translate } from '../../helpers/l10n';
+import { getLinkName } from '../../helpers/projectLinks';
+import { ProjectLink } from '../../types/types';
+import { ClearButton } from '../controls/buttons';
+import ProjectLinkIcon from '../icons/ProjectLinkIcon';
+
+interface Props {
+  iconOnly?: boolean;
+  link: ProjectLink;
+  // TODO Remove this prop when all links are migrated to the new design
+  miui?: boolean;
+}
+
+export default function MetaLink({ iconOnly, link, miui }: Props) {
+  const [expanded, setExpanded] = useState(false);
+
+  const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    setExpanded((expanded) => !expanded);
+  };
+
+  const handleCollapse = () => {
+    setExpanded(false);
+  };
+
+  const handleSelect = (event: React.MouseEvent<HTMLInputElement>) => {
+    event.currentTarget.select();
+  };
+
+  const linkTitle = getLinkName(link);
+  const isValid = isValidUri(link.url);
+  return miui ? (
+    <li>
+      <Link
+        isExternal
+        to={link.url}
+        preventDefault={!isValid}
+        onClick={isValid ? undefined : handleClick}
+        rel="nofollow noreferrer noopener"
+        target="_blank"
+        icon={<ProjectLinkIcon miui className="little-spacer-right" type={link.type} />}
+      >
+        {!iconOnly && linkTitle}
+      </Link>
+
+      {expanded && (
+        <div className="little-spacer-top display-flex-center">
+          <InputField
+            className="overview-key width-80"
+            onClick={handleSelect}
+            readOnly
+            type="text"
+            value={link.url}
+          />
+          <InteractiveIcon
+            Icon={CloseIcon}
+            aria-label={translate('hide')}
+            className="sw-ml-1"
+            onClick={handleCollapse}
+          />
+        </div>
+      )}
+    </li>
+  ) : (
+    <li>
+      <a
+        className="link-no-underline"
+        href={isValid ? link.url : undefined}
+        onClick={isValid ? undefined : handleClick}
+        rel="nofollow noreferrer noopener"
+        target="_blank"
+        title={linkTitle}
+      >
+        <ProjectLinkIcon className="little-spacer-right" type={link.type} />
+        {!iconOnly && linkTitle}
+      </a>
+      {expanded && (
+        <div className="little-spacer-top display-flex-center">
+          <input
+            className="overview-key width-80"
+            onClick={handleSelect}
+            readOnly
+            type="text"
+            value={link.url}
+          />
+          <ClearButton className="little-spacer-left" onClick={handleCollapse} />
+        </div>
+      )}
+    </li>
+  );
+}
index 86606a750d001a79e2a3d49f8660c98c3f4fd398..9392d6400e9d34e5c35625bc4722692bc4d965ab 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { LinkExternalIcon } from '@primer/octicons-react';
+import { HomeIcon } from 'design-system';
 import * as React from 'react';
 import BugTrackerIcon from './BugTrackerIcon';
 import ContinuousIntegrationIcon from './ContinuousIntegrationIcon';
@@ -27,19 +29,30 @@ import SCMIcon from './SCMIcon';
 
 interface ProjectLinkIconProps {
   type: string;
+  miui?: boolean;
 }
 
-export default function ProjectLinkIcon({ type, ...iconProps }: IconProps & ProjectLinkIconProps) {
-  switch (type) {
-    case 'issue':
-      return <BugTrackerIcon {...iconProps} />;
-    case 'homepage':
-      return <HouseIcon {...iconProps} />;
-    case 'ci':
-      return <ContinuousIntegrationIcon {...iconProps} />;
-    case 'scm':
-      return <SCMIcon {...iconProps} />;
-    default:
-      return <DetachIcon {...iconProps} />;
-  }
+export default function ProjectLinkIcon({
+  miui,
+  type,
+  ...iconProps
+}: IconProps & ProjectLinkIconProps) {
+  const getIcon = (): any => {
+    switch (type) {
+      case 'issue':
+        return BugTrackerIcon;
+      case 'homepage':
+        return miui ? HomeIcon : HouseIcon;
+      case 'ci':
+        return ContinuousIntegrationIcon;
+      case 'scm':
+        return SCMIcon;
+      default:
+        return miui ? LinkExternalIcon : DetachIcon;
+    }
+  };
+
+  const Icon = getIcon();
+
+  return <Icon {...iconProps} />;
 }
index 57699322713d689059272dd1ea88a6f416bfa066..7fd7a3df05c449740b8f98905916f3e07bb6cb15 100644 (file)
@@ -1825,12 +1825,23 @@ projects_management.filter_by_visibility=Filter by visibility
 
 project.info.title=Project Information
 application.info.title=Application Information
+project.about.title=About this Project
+application.about.title=About this Application
 project.info.description=Description
+project.info.empty_description=No description added for this project. 
+application.info.empty_description=No description added for this application. 
 project.info.quality_gate=Quality Gate used
 project.info.to_notifications=Set notifications
 project.info.notifications=Notifications
 project.info.main_branch=Main branch
 project.info.see_more_info_on_x_locs=See more information on your {0} lines of code
+project.info.make_home.title=Use as homepage
+project.info.make_home.label=Make this project my homepage
+application.info.make_home.label=Make this application my homepage
+project.info.make_home.tooltip=This means you'll be redirected to this project whenever you log in to sonarqube or click on the top-left SonarQube logo.
+application.info.make_home.tooltip=This means you'll be redirected to this application whenever you log in to sonarqube or click on the top-left SonarQube logo.
+overview.project_key.tooltip.TRK=Your project key is a unique identifier for your project. If you are using Maven, make sure the key matches the "groupId:artifactId" format.
+overview.project_key.tooltip.APP=Your application key is a unique identifier for your application.
 
 #------------------------------------------------------------------------------
 #