]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19018 Migrating project quality gate section to MIUI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Wed, 19 Apr 2023 09:33:28 +0000 (11:33 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 25 Apr 2023 20:03:01 +0000 (20:03 +0000)
34 files changed:
server/sonar-web/design-system/src/components/Accordion.tsx
server/sonar-web/design-system/src/components/Link.tsx
server/sonar-web/design-system/src/components/Separator.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/Text.tsx
server/sonar-web/design-system/src/components/buttons.tsx
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx
server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx
server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/SonarLintPromotion.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateCondition-test.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/styles.css
server/sonar-web/src/main/js/components/measure/Measure.tsx
server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/measure/utils.ts
server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx
server/sonar-web/src/main/js/components/shared/__tests__/DrilldownLink-test.tsx
server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/utils.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index d1af31d90961065af4f8af904067103543919a69..9dcfb92ab46fd5986de0f145096004cac818c93d 100644 (file)
@@ -24,6 +24,7 @@ import { BareButton } from './buttons';
 import { OpenCloseIndicator } from './icons/OpenCloseIndicator';
 
 interface AccordionProps {
+  ariaLabel?: string;
   children: React.ReactNode;
   className?: string;
   data?: string;
@@ -33,7 +34,7 @@ interface AccordionProps {
 }
 
 export function Accordion(props: AccordionProps) {
-  const { className, open, header, data, onClick } = props;
+  const { ariaLabel, className, open, header, data, onClick } = props;
 
   const id = React.useMemo(() => uniqueId('accordion-'), []);
   const handleClick = React.useCallback(() => {
@@ -50,6 +51,7 @@ export function Accordion(props: AccordionProps) {
       <BareButton
         aria-controls={`${id}-panel`}
         aria-expanded={open}
+        aria-label={ariaLabel}
         className="sw-flex sw-items-center sw-justify-between sw-px-2 sw-py-2 sw-box-border sw-w-full"
         id={`${id}-header`}
         onClick={handleClick}
index 91a88bd9d62da32ef7d7fe5a04f256398343ef88..237b9bf32237a95f9bde76acf70a64e1b2c8088c 100644 (file)
@@ -161,6 +161,18 @@ export const HoverLink = styled(StyledBaseLink)`
 `;
 HoverLink.displayName = 'HoverLink';
 
+export const LinkBox = styled(StyledBaseLink)`
+  text-decoration: none;
+
+  &:hover,
+  &:focus,
+  &:active {
+    background-color: ${themeColor('dropdownMenuHover')};
+    display: block;
+  }
+`;
+LinkBox.displayName = 'LinkBox';
+
 export const DiscreetLink = styled(HoverLink)`
   --border: ${themeBorder('default', 'linkDiscreet')};
 `;
diff --git a/server/sonar-web/design-system/src/components/Separator.tsx b/server/sonar-web/design-system/src/components/Separator.tsx
new file mode 100644 (file)
index 0000000..5f9a4d6
--- /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 styled from '@emotion/styled';
+import tw from 'twin.macro';
+import { themeColor } from '../helpers/theme';
+
+export const BasicSeparator = styled.hr`
+  height: 1px;
+  background-color: ${themeColor('border')};
+
+  ${tw`sw-my-1`}
+  ${tw`sw-overflow-hidden`};
+  ${tw`sw-clear-both`}
+`;
+
+export const BlueGreySeparator = styled(BasicSeparator)`
+  background-color: ${themeColor('popupBorder')};
+`;
+
+export const GreySeparator = styled(BasicSeparator)`
+  background-color: ${themeColor('subnavigationBorder')};
+`;
index e83f2198f4d57217567610c603b1dcffe61c7dec..540074b8bc8d218044a082ea0b6f82ae985eccd9 100644 (file)
@@ -50,6 +50,22 @@ export function TextMuted({ text, className }: { className?: string; text: strin
   );
 }
 
+export function PageTitle({ text, className }: { className?: string; text: string }) {
+  return (
+    <StyledPageTitle className={className} title={text}>
+      {text}
+    </StyledPageTitle>
+  );
+}
+
+export function TextError({ text, className }: { className?: string; text: string }) {
+  return (
+    <StyledTextError className={className} title={text}>
+      {text}
+    </StyledTextError>
+  );
+}
+
 export const StyledText = styled.span`
   ${tw`sw-inline-block`};
   ${tw`sw-truncate`};
@@ -68,3 +84,12 @@ const StyledMutedText = styled(StyledText)`
   ${tw`sw-font-regular`};
   color: ${themeColor('dropdownMenuSubTitle')};
 `;
+
+const StyledPageTitle = styled(StyledText)`
+  ${tw`sw-text-base`}
+  color: ${themeColor('facetHeader')};
+`;
+
+const StyledTextError = styled(StyledText)`
+  color: ${themeColor('danger')};
+`;
index b939dbbe4c05447eb1e5187785056494eb084564..13304f9215ccf468efddfe0973664219c80abd48 100644 (file)
@@ -216,4 +216,8 @@ const ThirdPartyButtonStyled: React.FC<ButtonProps> = styled(Button)`
 export const BareButton = styled.button`
   all: unset;
   cursor: pointer;
+
+  &:focus-visible {
+    background-color: ${themeColor('dropdownMenuHover')};
+  }
 `;
index cda65984092ef15072738d6980d7449b3e7476e3..c4c52b9436af5cb7b75f56a7ac20306b0f0666d2 100644 (file)
@@ -42,6 +42,7 @@ export * from './MetricsRatingBadge';
 export * from './NavBarTabs';
 export * from './NewCodeLegend';
 export { QualityGateIndicator } from './QualityGateIndicator';
+export * from './Separator';
 export * from './SizeIndicator';
 export * from './SonarQubeLogo';
 export * from './Text';
index 5cc97dbd2b6db71bba55ae395f2508265d9eb212..3b194749b6aa49aec8f3248e43d97db62d3ebd47 100644 (file)
@@ -92,8 +92,8 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
           {projectIsEmpty ? (
             <NoCodeWarning branchLike={branch} component={component} measures={measures} />
           ) : (
-            <div className="display-flex-row">
-              <div className="width-25 big-spacer-right">
+            <div className="sw-flex">
+              <div className="width-30 sw-mr-12">
                 <QualityGatePanel
                   component={component}
                   loading={loadingStatus}
@@ -101,8 +101,8 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
                 />
               </div>
 
-              <div className="flex-1">
-                <div className="display-flex-column">
+              <div className="sw-flex-1">
+                <div className="sw-flex sw-flex-col">
                   <MeasuresPanel
                     appLeak={appLeak}
                     branch={branch}
index 1eab5de6fdfd075c2c239b9739e7d1a39c92771b..063142c02b58d4808ade8c4af0b0819ace6ddeb4 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 classNames from 'classnames';
+import { BasicSeparator, Card, DeferredSpinner } from 'design-system';
 import { flatMap } from 'lodash';
 import * as React from 'react';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
-import { Alert } from '../../../components/ui/Alert';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { ComponentQualifier, isApplication } from '../../../types/component';
 import { QualityGateStatus } from '../../../types/quality-gates';
 import { CaycStatus, Component } from '../../../types/types';
+import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
+import QualityGateStatusHeader from '../components/QualityGateStatusHeader';
+import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView';
+import { QualityGateStatusTitle } from '../components/QualityGateStatusTitle';
 import SonarLintPromotion from '../components/SonarLintPromotion';
 import ApplicationNonCaycProjectWarning from './ApplicationNonCaycProjectWarning';
 import QualityGatePanelSection from './QualityGatePanelSection';
@@ -69,88 +69,56 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
     qgStatuses.some((p) => Boolean(p.ignoredConditions));
 
   return (
-    <div className="overview-panel" data-test="overview__quality-gate-panel">
-      <div className="display-flex-center spacer-bottom">
-        <h2 className="overview-panel-title null-spacer-bottom">
-          {translate('overview.quality_gate')}{' '}
-        </h2>
-        <HelpTooltip
-          className="little-spacer-left"
-          overlay={
-            <div className="big-padded-top big-padded-bottom">
-              {translate('overview.quality_gate.help')}
+    <div data-test="overview__quality-gate-panel">
+      <QualityGateStatusTitle />
+      <Card>
+        <div>
+          {loading ? (
+            <div className="sw-p-6">
+              <DeferredSpinner loading={loading} />
             </div>
-          }
-        />
-      </div>
-      {showIgnoredConditionWarning && (
-        <Alert className="big-spacer-bottom" display="inline" variant="info">
-          <span className="text-middle">
-            {translate('overview.quality_gate.ignored_conditions')}
-          </span>
-          <HelpTooltip
-            className="spacer-left"
-            overlay={translate('overview.quality_gate.ignored_conditions.tooltip')}
-          />
-        </Alert>
-      )}
+          ) : (
+            <>
+              <QualityGateStatusHeader
+                status={overallLevel}
+                failedConditionCount={overallFailedConditionsCount}
+              />
+              {success && <QualityGateStatusPassedView />}
 
-      <div>
-        {loading ? (
-          <div className="overview-panel-big-padded">
-            <DeferredSpinner loading={loading} />
-          </div>
-        ) : (
-          <>
-            <div
-              className={classNames('overview-quality-gate-badge-large', {
-                failed: !success,
-                success,
-              })}
-            >
-              <div className="big-spacer-bottom huge h3">
-                {translate('metric.level', overallLevel)}
-              </div>
+              {showIgnoredConditionWarning && <IgnoredConditionWarning />}
 
-              <span className="small">
-                {overallFailedConditionsCount > 0
-                  ? translateWithParameters(
-                      'overview.X_conditions_failed',
-                      overallFailedConditionsCount
-                    )
-                  : translate('overview.quality_gate_all_conditions_passed')}
-              </span>
-            </div>
+              {!success && <BasicSeparator />}
 
-            {(overallFailedConditionsCount > 0 ||
-              qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && (
-              <div data-test="overview__quality-gate-conditions">
-                {qgStatuses.map((qgStatus) => (
-                  <QualityGatePanelSection
-                    component={component}
-                    key={qgStatus.key}
-                    qgStatus={qgStatus}
-                  />
-                ))}
-              </div>
-            )}
+              {(overallFailedConditionsCount > 0 ||
+                qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && (
+                <div data-test="overview__quality-gate-conditions">
+                  {qgStatuses.map((qgStatus) => (
+                    <QualityGatePanelSection
+                      component={component}
+                      key={qgStatus.key}
+                      qgStatus={qgStatus}
+                    />
+                  ))}
+                </div>
+              )}
+            </>
+          )}
+        </div>
+      </Card>
 
-            {nonCaycProjectsInApp.length > 0 && (
-              <ApplicationNonCaycProjectWarning
-                projects={nonCaycProjectsInApp}
-                caycStatus={CaycStatus.NonCompliant}
-              />
-            )}
+      {nonCaycProjectsInApp.length > 0 && (
+        <ApplicationNonCaycProjectWarning
+          projects={nonCaycProjectsInApp}
+          caycStatus={CaycStatus.NonCompliant}
+        />
+      )}
 
-            {overCompliantCaycProjectsInApp.length > 0 && (
-              <ApplicationNonCaycProjectWarning
-                projects={overCompliantCaycProjectsInApp}
-                caycStatus={CaycStatus.OverCompliant}
-              />
-            )}
-          </>
-        )}
-      </div>
+      {overCompliantCaycProjectsInApp.length > 0 && (
+        <ApplicationNonCaycProjectWarning
+          projects={overCompliantCaycProjectsInApp}
+          caycStatus={CaycStatus.OverCompliant}
+        />
+      )}
       <SonarLintPromotion
         qgConditions={flatMap(qgStatuses, (qgStatus) => qgStatus.failedConditions)}
       />
index faedae61120d6b3f90491b0694a98e8c9cb5bdb5..d4ceaf05679df5417120a18cd57586d2ed0be36a 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 { Accordion, BasicSeparator, TextMuted } from 'design-system';
 import * as React from 'react';
-import { ButtonPlain } from '../../../components/controls/buttons';
-import ChevronDownIcon from '../../../components/icons/ChevronDownIcon';
-import ChevronRightIcon from '../../../components/icons/ChevronRightIcon';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translateWithParameters } from '../../../helpers/l10n';
 import { isDiffMetric } from '../../../helpers/measures';
 import { BranchLike } from '../../../types/branch-like';
 import { isApplication } from '../../../types/component';
@@ -57,19 +55,6 @@ function splitConditions(
   return [newCodeFailedConditions, overallFailedConditions];
 }
 
-function displayConditions(conditions: number) {
-  if (conditions === 0) {
-    return null;
-  }
-
-  const text =
-    conditions === 1
-      ? translate('overview.1_condition_failed')
-      : translateWithParameters('overview.X_conditions_failed', conditions);
-
-  return <span className="text-muted big-spacer-left">{text}</span>;
-}
-
 export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
   const { component, qgStatus } = props;
   const [collapsed, setCollapsed] = React.useState(false);
@@ -96,7 +81,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
     qgStatus.failedConditions
   );
 
-  const showName = isApplication(component.qualifier);
+  const collapsible = isApplication(component.qualifier);
 
   const showSectionTitles =
     isApplication(component.qualifier) ||
@@ -107,82 +92,107 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
     ? translateWithParameters('overview.quality_gate.show_project_conditions_x', qgStatus.name)
     : translateWithParameters('overview.quality_gate.hide_project_conditions_x', qgStatus.name);
 
-  return (
-    <div className="overview-quality-gate-conditions">
-      {showName && (
-        <ButtonPlain
-          aria-label={toggleLabel}
-          aria-expanded={!collapsed}
-          className="width-100 text-left"
-          onClick={toggle}
-        >
-          <div className="display-flex-center">
-            <div
-              className="overview-quality-gate-conditions-project-name text-ellipsis h3"
-              title={qgStatus.name}
-            >
-              {collapsed ? <ChevronRightIcon /> : <ChevronDownIcon />}
-              <span className="spacer-left">{qgStatus.name}</span>
-            </div>
-            {collapsed && displayConditions(qgStatus.failedConditions.length)}
-          </div>
-        </ButtonPlain>
-      )}
+  const renderFailedConditions = () => {
+    return (
+      <>
+        {newCodeFailedConditions.length > 0 && (
+          <>
+            {showSectionTitles && (
+              <>
+                <p className="sw-px-2 sw-py-3">
+                  {translateWithParameters(
+                    'quality_gates.conditions.new_code_x',
+                    newCodeFailedConditions.length.toString()
+                  )}
+                </p>
+                <BasicSeparator />
+              </>
+            )}
+            <QualityGateConditions
+              component={qgStatus}
+              branchLike={qgStatus.branchLike}
+              failedConditions={newCodeFailedConditions}
+            />
+          </>
+        )}
+
+        {overallFailedConditions.length > 0 && (
+          <>
+            {showSectionTitles && (
+              <>
+                <p className="sw-px-2 sw-py-3">
+                  {translateWithParameters(
+                    'quality_gates.conditions.overall_code_x',
+                    overallFailedConditions.length.toString()
+                  )}
+                </p>
+                <BasicSeparator />
+              </>
+            )}
+            <QualityGateConditions
+              component={qgStatus}
+              branchLike={qgStatus.branchLike}
+              failedConditions={overallFailedConditions}
+            />
+          </>
+        )}
+      </>
+    );
+  };
 
-      {!collapsed && (
+  return (
+    <>
+      {collapsible ? (
+        <>
+          <Accordion
+            ariaLabel={toggleLabel}
+            onClick={toggle}
+            open={!collapsed}
+            header={
+              <div className="sw-flex sw-flex-col sw-text-sm">
+                <span className="sw-body-sm-highlight">{qgStatus.name}</span>
+                {collapsed && newCodeFailedConditions.length > 0 && (
+                  <TextMuted
+                    text={translateWithParameters(
+                      'quality_gates.conditions.new_code_x',
+                      newCodeFailedConditions.length
+                    )}
+                  />
+                )}
+                {collapsed && overallFailedConditions.length > 0 && (
+                  <TextMuted
+                    text={translateWithParameters(
+                      'quality_gates.conditions.overall_code_x',
+                      overallFailedConditions.length
+                    )}
+                  />
+                )}
+              </div>
+            }
+          >
+            <BasicSeparator />
+            {renderFailedConditions()}
+          </Accordion>
+          <BasicSeparator />
+        </>
+      ) : (
         <>
+          {renderFailedConditions()}
           {qgStatus.caycStatus === CaycStatus.NonCompliant &&
             !isApplication(component.qualifier) && (
               <div className="big-padded bordered-bottom overview-quality-gate-conditions-list">
                 <CleanAsYouCodeWarning component={component} />
               </div>
             )}
-
           {qgStatus.caycStatus === CaycStatus.OverCompliant &&
             !isApplication(component.qualifier) && (
               <div className="big-padded bordered-bottom overview-quality-gate-conditions-list">
                 <CleanAsYouCodeWarningOverCompliant component={component} />
               </div>
             )}
-
-          {newCodeFailedConditions.length > 0 && (
-            <>
-              {showSectionTitles && (
-                <div className="big-padded overview-quality-gate-conditions-section-title h4">
-                  {translateWithParameters(
-                    'quality_gates.conditions.new_code_x',
-                    newCodeFailedConditions.length.toString()
-                  )}
-                </div>
-              )}
-              <QualityGateConditions
-                component={qgStatus}
-                branchLike={qgStatus.branchLike}
-                failedConditions={newCodeFailedConditions}
-              />
-            </>
-          )}
-
-          {overallFailedConditions.length > 0 && (
-            <>
-              {showSectionTitles && (
-                <div className="big-padded overview-quality-gate-conditions-section-title h4">
-                  {translateWithParameters(
-                    'quality_gates.conditions.overall_code_x',
-                    overallFailedConditions.length.toString()
-                  )}
-                </div>
-              )}
-              <QualityGateConditions
-                component={qgStatus}
-                branchLike={qgStatus.branchLike}
-                failedConditions={overallFailedConditions}
-              />
-            </>
-          )}
         </>
       )}
-    </div>
+    </>
   );
 }
 
index a96481a7a597ba345fd358d26f768f9672977715..1aae810e9a1bd7a92a502e6439f80a9797c5b525 100644 (file)
@@ -205,7 +205,7 @@ describe('project overview', () => {
 
     // QG panel
     expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
-    expect(screen.getByText('overview.quality_gate_all_conditions_passed')).toBeInTheDocument();
+    expect(screen.getByText('overview.passed.clean_code')).toBeInTheDocument();
     expect(
       screen.queryByText('overview.quality_gate.conditions.cayc.warning')
     ).not.toBeInTheDocument();
@@ -342,7 +342,7 @@ it.each([
     renderBranchOverview();
 
     // wait for loading
-    await screen.findByText('overview.quality_gate');
+    await screen.findByText('overview.quality_gate.status');
 
     expect(screen.queryByText('overview.project.next_steps.set_up_ci') === null).toBe(expected);
   }
diff --git a/server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx b/server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx
new file mode 100644 (file)
index 0000000..a3055cf
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 { FlagMessage, HelperHintIcon } from 'design-system';
+import React from 'react';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import { translate } from '../../../helpers/l10n';
+
+export default function IgnoredConditionWarning() {
+  return (
+    <FlagMessage
+      ariaLabel={translate('overview.quality_gate.ignored_conditions')}
+      className="sw-mb-4"
+      variant="info"
+    >
+      <span>{translate('overview.quality_gate.ignored_conditions')}</span>
+      <HelpTooltip
+        className="sw-ml-2"
+        overlay={translate('overview.quality_gate.ignored_conditions.tooltip')}
+      >
+        <HelperHintIcon aria-label="help-tooltip" />
+      </HelpTooltip>
+    </FlagMessage>
+  );
+}
index 8e6f8bd1f5a27958b1b1b470db20f7fc78f5b091..73476f95598072da2b305cf27a060aa0bfc06a3e 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 classNames from 'classnames';
+import { LinkBox, TextMuted } from 'design-system';
 import * as React from 'react';
 import { Path } from 'react-router-dom';
-import Link from '../../../components/common/Link';
 import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
-import Measure from '../../../components/measure/Measure';
-import DrilldownLink from '../../../components/shared/DrilldownLink';
+import MeasureIndicator from '../../../components/measure/MeasureIndicator';
+import { isIssueMeasure, propsToIssueParams } from '../../../components/shared/utils';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
 import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures';
-import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
+import {
+  getComponentDrilldownUrl,
+  getComponentIssuesUrl,
+  getComponentSecurityHotspotsUrl,
+} from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
 import { IssueType } from '../../../types/issues';
-import { MetricKey } from '../../../types/metrics';
+import { MetricKey, MetricType } from '../../../types/metrics';
 import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
 import { Component, Dict } from '../../../types/types';
 
@@ -87,11 +90,6 @@ export default class QualityGateCondition extends React.PureComponent<Props> {
   wrapWithLink(children: React.ReactNode) {
     const { branchLike, component, condition } = this.props;
 
-    const className = classNames(
-      'overview-quality-gate-condition',
-      `overview-quality-gate-condition-${condition.level.toLowerCase()}`
-    );
-
     const metricKey = condition.measure.metric.key;
 
     const METRICS_TO_URL_MAPPING: Dict<() => Path> = {
@@ -109,67 +107,83 @@ export default class QualityGateCondition extends React.PureComponent<Props> {
       [MetricKey.new_security_hotspots_reviewed]: () => this.getUrlForSecurityHotspot(true),
     };
 
-    return (
-      <li>
-        {METRICS_TO_URL_MAPPING[metricKey] ? (
-          <Link className={className} to={METRICS_TO_URL_MAPPING[metricKey]()}>
-            {children}
-          </Link>
-        ) : (
-          <DrilldownLink
-            branchLike={branchLike}
-            className={className}
-            component={component.key}
-            metric={condition.measure.metric.key}
-            inNewCodePeriod={condition.period != null}
-          >
-            {children}
-          </DrilldownLink>
-        )}
-      </li>
-    );
+    if (METRICS_TO_URL_MAPPING[metricKey]) {
+      return <LinkBox to={METRICS_TO_URL_MAPPING[metricKey]()}>{children}</LinkBox>;
+    }
+
+    if (isIssueMeasure(condition.measure.metric.key)) {
+      const url = getComponentIssuesUrl(component.key, {
+        ...propsToIssueParams(condition.measure.metric.key, condition.period != null),
+        ...getBranchLikeQuery(branchLike),
+      });
+
+      return <LinkBox to={url}>{children}</LinkBox>;
+    }
+
+    const url = getComponentDrilldownUrl({
+      componentKey: component.key,
+      metric: condition.measure.metric.key,
+      branchLike,
+      listView: true,
+    });
+
+    return <LinkBox to={url}>{children}</LinkBox>;
   }
 
-  render() {
+  getPrimaryText = () => {
     const { condition } = this.props;
     const { measure } = condition;
     const { metric } = measure;
-
     const isDiff = isDiffMetric(metric.key);
 
+    const subText =
+      !isDiff && condition.period != null
+        ? `${localizeMetric(metric.key)} ${translate('quality_gates.conditions.new_code')}`
+        : localizeMetric(metric.key);
+
+    if (metric.type !== MetricType.Rating) {
+      const actual = (condition.period ? measure.period?.value : measure.value) as string;
+      const formattedValue = formatMeasure(actual, metric.type, {
+        decimal: 2,
+        omitExtraDecimalZeros: metric.type === MetricType.Percent,
+      });
+      return `${formattedValue} ${subText}`;
+    }
+
+    return subText;
+  };
+
+  render() {
+    const { condition } = this.props;
+    const { measure } = condition;
+    const { metric } = measure;
+
     const threshold = (condition.level === 'ERROR' ? condition.error : condition.warning) as string;
     const actual = (condition.period ? measure.period?.value : measure.value) as string;
 
     let operator = translate('quality_gates.operator', condition.op);
 
-    if (metric.type === 'RATING') {
+    if (metric.type === MetricType.Rating) {
       operator = translate('quality_gates.operator', condition.op, 'rating');
     }
 
     return this.wrapWithLink(
-      <div className="overview-quality-gate-condition-container display-flex-center">
-        <div className="overview-quality-gate-condition-value text-center spacer-right">
-          <Measure
-            decimals={2}
-            metricKey={measure.metric.key}
-            metricType={measure.metric.type}
-            value={actual}
-          />
-        </div>
-
-        <div>
-          <span className="overview-quality-gate-condition-metric little-spacer-right">
-            <IssueTypeIcon className="little-spacer-right" query={metric.key} />
-            {localizeMetric(metric.key)}
-          </span>
-          {!isDiff && condition.period != null && (
-            <span className="overview-quality-gate-condition-period text-ellipsis little-spacer-right">
-              {translate('quality_gates.conditions.new_code')}
+      <div className="sw-flex sw-items-center sw-p-2">
+        <MeasureIndicator
+          className="sw-flex sw-justify-center sw-w-6 sw-mx-4"
+          decimals={2}
+          metricKey={measure.metric.key}
+          metricType={measure.metric.type}
+          value={actual}
+        />
+        <div className="sw-flex sw-flex-col sw-text-sm">
+          <div className="sw-flex sw-items-center">
+            <IssueTypeIcon className="sw-mr-2" query={metric.key} />
+            <span className="sw-body-sm-highlight sw-text-ellipsis sw-max-w-abs-300">
+              {this.getPrimaryText()}
             </span>
-          )}
-          <span className="little-spacer-top small text-muted">
-            {operator} {formatMeasure(threshold, metric.type)}
-          </span>
+          </div>
+          <TextMuted text={`${operator} ${formatMeasure(threshold, metric.type)}`} />
         </div>
       </div>
     );
index c9e52c9283ccef0dd59d417766bc1f87d1dad4df..0fe8c6ded695f091af8e463c959255fc6c0c6e70 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 { BasicSeparator, Link } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
-import { ButtonLink } from '../../../components/controls/buttons';
-import ChevronDownIcon from '../../../components/icons/ChevronDownIcon';
-import { translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
 import { BranchLike } from '../../../types/branch-like';
 import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
 import { Component } from '../../../types/types';
@@ -50,6 +49,7 @@ export function QualityGateConditions(props: QualityGateConditionsProps) {
 
   let renderConditions;
   let renderCollapsed;
+
   if (collapsed && sortedConditions.length > MAX_CONDITIONS) {
     renderConditions = sortedConditions.slice(0, MAX_CONDITIONS);
     renderCollapsed = true;
@@ -59,30 +59,22 @@ export function QualityGateConditions(props: QualityGateConditionsProps) {
   }
 
   return (
-    <ul
-      className="overview-quality-gate-conditions-list"
-      id="overview-quality-gate-conditions-list"
-    >
+    <ul id="overview-quality-gate-conditions-list" className="sw-mb-2">
       {renderConditions.map((condition) => (
-        <QualityGateCondition
-          branchLike={branchLike}
-          component={component}
-          condition={condition}
-          key={condition.measure.metric.key}
-        />
+        <div key={condition.measure.metric.key}>
+          <QualityGateCondition
+            branchLike={branchLike}
+            component={component}
+            condition={condition}
+          />
+          <BasicSeparator />
+        </div>
       ))}
       {renderCollapsed && (
-        <li>
-          <ButtonLink
-            className="overview-quality-gate-conditions-list-collapse"
-            onClick={handleToggleCollapsed}
-          >
-            {translateWithParameters(
-              'overview.X_more_failed_conditions',
-              sortedConditions.length - MAX_CONDITIONS
-            )}
-            <ChevronDownIcon className="little-spacer-left" />
-          </ButtonLink>
+        <li className="sw-flex sw-justify-center sw-my-3">
+          <Link onClick={handleToggleCollapsed} to={{}} preventDefault={true}>
+            <span className="sw-font-semibold sw-text-sm">{translate('show_more')}</span>
+          </Link>
         </li>
       )}
     </ul>
diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx
new file mode 100644 (file)
index 0000000..bfe09a0
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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 { QualityGateIndicator, TextError, TextMuted } from 'design-system';
+import React from 'react';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Status } from '../../../types/types';
+
+interface Props {
+  status: Status;
+  failedConditionCount: number;
+}
+
+export default function QualityGateStatusHeader(props: Props) {
+  const { status, failedConditionCount } = props;
+
+  return (
+    <div className="sw-flex sw-items-center sw-mb-4">
+      <QualityGateIndicator status={status} className="sw-mr-2" size="xl" />
+      <div className="sw-flex sw-flex-col">
+        <div>
+          <TextMuted text={translate('overview.quality_gate')} />
+        </div>
+        <div>
+          <span className="sw-heading-lg">{translate('metric.level', status)}</span>
+        </div>
+      </div>
+      <div className="sw-flex sw-flex-1 sw-justify-end">
+        {failedConditionCount > 0 && (
+          <TextError
+            text={translateWithParameters('overview.X_conditions_failed', failedConditionCount)}
+          />
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx
new file mode 100644 (file)
index 0000000..bc8fe37
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 { OverviewQGPassedIcon } from 'design-system';
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default function QualityGateStatusPassedView() {
+  return (
+    <div className="sw-flex sw-items-center sw-justify-center sw-flex-col">
+      <OverviewQGPassedIcon className="sw-my-12" />
+      <p className="sw-mb-8">{translate('overview.passed.clean_code')}</p>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx
new file mode 100644 (file)
index 0000000..55010a5
--- /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 { HelperHintIcon, PageTitle } from 'design-system';
+import React from 'react';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import { translate } from '../../../helpers/l10n';
+
+export function QualityGateStatusTitle() {
+  return (
+    <div className="sw-flex sw-items-center sw-mb-4">
+      <PageTitle text={translate('overview.quality_gate.status')} />
+      <HelpTooltip
+        className="sw-ml-2"
+        overlay={<div className="sw-my-4">{translate('overview.quality_gate.help')}</div>}
+      >
+        <HelperHintIcon aria-label="help-tooltip" />
+      </HelpTooltip>
+    </div>
+  );
+}
index b28c0324f0aeb0681310beb9ffd97e494e1a52eb..67a41dc2c348cd16701bc435644b22d8a31bad70 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Card, DiscreetLink } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
@@ -55,26 +56,27 @@ export function SonarLintPromotion({ currentUser, qgConditions }: SonarLintPromo
     return null;
   }
   return (
-    <div className="it__overview__sonarlint-promotion big-spacer-top overview-quality-gate-sonar-lint-info">
+    <Card className="it__overview__sonarlint-promotion sw-my-4 sw-body-sm">
       <FormattedMessage
         id="overview.fix_failed_conditions_with_sonarlint"
         defaultMessage={translate('overview.fix_failed_conditions_with_sonarlint')}
         values={{
           link: (
             <>
-              <a
-                href="https://www.sonarqube.org/sonarlint/?referrer=sonarqube"
+              <DiscreetLink
+                to="https://www.sonarqube.org/sonarlint/?referrer=sonarqube"
                 rel="noopener noreferrer"
                 target="_blank"
+                showExternalIcon={false}
               >
                 SonarLint
-              </a>
+              </DiscreetLink>
               <SonarLintIcon size={16} />
             </>
           ),
         }}
       />
-    </div>
+    </Card>
   );
 }
 
index e7d521b6a8c4bd62719d9fc5e970c2107d90a08c..979b58c89768834e2f636a2adfc832ce774b9e7c 100644 (file)
@@ -23,12 +23,11 @@ import { mockBranch } from '../../../../helpers/mocks/branch-like';
 import { mockQualityGateStatusConditionEnhanced } from '../../../../helpers/mocks/quality-gates';
 import { mockMetric } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { MetricKey } from '../../../../types/metrics';
+import { MetricKey, MetricType } from '../../../../types/metrics';
 import { QualityGateStatusConditionEnhanced } from '../../../../types/quality-gates';
 import QualityGateCondition from '../QualityGateCondition';
 
 it.each([
-  [quickMock(MetricKey.open_issues, 'INT')],
   [quickMock(MetricKey.reliability_rating)],
   [quickMock(MetricKey.security_rating)],
   [quickMock(MetricKey.sqale_rating)],
@@ -51,6 +50,11 @@ it.each([
   // }
 });
 
+it('should show the count when metric is not rating', async () => {
+  renderQualityGateCondition({ condition: quickMock(MetricKey.open_issues, MetricType.Integer) });
+  expect(await screen.findByText('3 metric.open_issues.name')).toBeInTheDocument();
+});
+
 it('should work with branch', async () => {
   const condition = quickMock(MetricKey.new_maintainability_rating);
   renderQualityGateCondition({ branchLike: mockBranch(), condition });
index ef967ebf969c55a488f8cf5edcfe3c190174aee5..ed4fd8bb5b57fe2d34b97ea0b1a96136ef4effd5 100644 (file)
@@ -48,7 +48,7 @@ it('should be collapsible', async () => {
     HALF_CONDITIONS
   );
 
-  await user.click(screen.getByRole('button', { name: 'overview.X_more_failed_conditions.5' }));
+  await user.click(screen.getByRole('link', { name: 'show_more' }));
 
   expect(await screen.findAllByText(/.*metric..+.name.*/)).toHaveLength(ALL_CONDITIONS);
   expect(await screen.findAllByText('quality_gates.operator', { exact: false })).toHaveLength(
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx
deleted file mode 100644 (file)
index 95802c8..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import classNames from 'classnames';
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { colors } from '../../../app/theme';
-import Link from '../../../components/common/Link';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
-import HelpIcon from '../../../components/icons/HelpIcon';
-import { translate } from '../../../helpers/l10n';
-import { getQualityGatesUrl, getQualityGateUrl } from '../../../helpers/urls';
-import { Component, Status } from '../../../types/types';
-
-interface Props {
-  component: Component;
-  level?: Status;
-}
-
-export function LargeQualityGateBadge({ component, level }: Props) {
-  const success = level === 'OK';
-
-  const path =
-    component.qualityGate === undefined
-      ? getQualityGatesUrl()
-      : getQualityGateUrl(component.qualityGate.name);
-
-  return (
-    <div
-      className={classNames('overview-quality-gate-badge-large small', {
-        failed: !success,
-        success,
-      })}
-    >
-      <div className="display-flex-center">
-        <span>{translate('overview.on_new_code_long')}</span>
-
-        <HelpTooltip
-          className="little-spacer-left"
-          overlay={
-            <FormattedMessage
-              defaultMessage={translate('overview.quality_gate.conditions_on_new_code')}
-              id="overview.quality_gate.conditions_on_new_code"
-              values={{
-                link: <Link to={path}>{translate('overview.quality_gate')}</Link>,
-              }}
-            />
-          }
-        >
-          <HelpIcon fill={colors.transparentWhite} size={12} />
-        </HelpTooltip>
-      </div>
-      {level !== undefined && (
-        <div className="huge-spacer-top huge h3">{translate('metric.level', level)}</div>
-      )}
-    </div>
-  );
-}
-
-export default React.memo(LargeQualityGateBadge);
index 1fe518c34933ae9c259a3cb8de7b3343b595c83d..03717947f1d48b0d39314533352dafd34c82d357 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 classNames from 'classnames';
+import {
+  BasicSeparator,
+  Card,
+  DeferredSpinner,
+  HelperHintIcon,
+  LargeCenteredLayout,
+  Link,
+  TextMuted,
+} from 'design-system';
 import { differenceBy, uniq } from 'lodash';
 import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
 import { getMeasuresWithMetrics } from '../../../api/measures';
 import { BranchStatusContextInterface } from '../../../app/components/branch-status/BranchStatusContext';
 import withBranchStatus from '../../../app/components/branch-status/withBranchStatus';
 import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
-import { Alert } from '../../../components/ui/Alert';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
 import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
 import { isDefined } from '../../../helpers/types';
+import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls';
 import { BranchStatusData, PullRequest } from '../../../types/branch-like';
 import { IssueType } from '../../../types/issues';
 import { Component, MeasureEnhanced } from '../../../types/types';
+import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
 import IssueLabel from '../components/IssueLabel';
 import IssueRating from '../components/IssueRating';
 import MeasurementLabel from '../components/MeasurementLabel';
 import QualityGateConditions from '../components/QualityGateConditions';
+import QualityGateStatusHeader from '../components/QualityGateStatusHeader';
+import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView';
+import { QualityGateStatusTitle } from '../components/QualityGateStatusTitle';
 import SonarLintPromotion from '../components/SonarLintPromotion';
 import '../styles.css';
 import { MeasurementType, PR_METRICS } from '../utils';
 import AfterMergeEstimate from './AfterMergeEstimate';
-import LargeQualityGateBadge from './LargeQualityGateBadge';
 
 interface Props extends BranchStatusData, Pick<BranchStatusContextInterface, 'fetchBranchStatus'> {
   branchLike: PullRequest;
@@ -140,9 +152,11 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {
 
     if (loading) {
       return (
-        <div className="page page-limited">
-          <i className="spinner" />
-        </div>
+        <LargeCenteredLayout>
+          <div className="sw-p-6">
+            <DeferredSpinner loading={true} />
+          </div>
+        </LargeCenteredLayout>
       );
     }
 
@@ -150,61 +164,70 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {
       return null;
     }
 
+    const path =
+      component.qualityGate === undefined
+        ? getQualityGatesUrl()
+        : getQualityGateUrl(component.qualityGate.name);
+
     const failedConditions = conditions
       .filter((condition) => condition.level === 'ERROR')
       .map((c) => enhanceConditionWithMeasure(c, measures))
       .filter(isDefined);
 
     return (
-      <div className="page page-limited">
-        <div
-          className={classNames('pr-overview', {
-            'has-conditions': failedConditions.length > 0,
-          })}
-        >
-          {ignoredConditions && (
-            <Alert className="big-spacer-bottom" display="inline" variant="info">
-              <span className="text-middle">
-                {translate('overview.quality_gate.ignored_conditions')}
-              </span>
-              <HelpTooltip
-                className="spacer-left"
-                overlay={translate('overview.quality_gate.ignored_conditions.tooltip')}
-              />
-            </Alert>
-          )}
-          <div className="display-flex-row">
-            <div className="big-spacer-right">
-              <h2 className="overview-panel-title spacer-bottom small display-inline-flex-center">
-                {translate('overview.quality_gate')}
-                <HelpTooltip
-                  className="little-spacer-left"
-                  overlay={
-                    <div className="big-padded-top big-padded-bottom">
-                      {translate('overview.quality_gate.help')}
-                    </div>
-                  }
-                />
-              </h2>
-              <LargeQualityGateBadge component={component} level={status} />
+      <LargeCenteredLayout>
+        <div className="it__pr-overview sw-mt-12">
+          <div className="sw-flex">
+            <div className="sw-flex sw-flex-col sw-mr-12 width-30">
+              <QualityGateStatusTitle />
+              <Card>
+                {status && (
+                  <QualityGateStatusHeader
+                    status={status}
+                    failedConditionCount={failedConditions.length}
+                  />
+                )}
+
+                <div className="sw-flex sw-items-center sw-mb-4">
+                  <TextMuted text={translate('overview.on_new_code_long')} />
+                  <HelpTooltip
+                    className="sw-ml-2"
+                    overlay={
+                      <FormattedMessage
+                        defaultMessage={translate('overview.quality_gate.conditions_on_new_code')}
+                        id="overview.quality_gate.conditions_on_new_code"
+                        values={{
+                          link: <Link to={path}>{translate('overview.quality_gate.status')}</Link>,
+                        }}
+                      />
+                    }
+                  >
+                    <HelperHintIcon aria-label="help-tooltip" />
+                  </HelpTooltip>
+                </div>
 
+                {ignoredConditions && <IgnoredConditionWarning />}
+
+                {status === 'OK' && failedConditions.length === 0 && (
+                  <QualityGateStatusPassedView />
+                )}
+
+                {status !== 'OK' && <BasicSeparator />}
+
+                {failedConditions.length > 0 && (
+                  <div>
+                    <QualityGateConditions
+                      branchLike={branchLike}
+                      collapsible={true}
+                      component={component}
+                      failedConditions={failedConditions}
+                    />
+                  </div>
+                )}
+              </Card>
               <SonarLintPromotion qgConditions={conditions} />
             </div>
 
-            {failedConditions.length > 0 && (
-              <div className="pr-overview-failed-conditions big-spacer-right">
-                <h2 className="overview-panel-title spacer-bottom small">
-                  {translate('overview.failed_conditions')}
-                </h2>
-                <QualityGateConditions
-                  branchLike={branchLike}
-                  collapsible={true}
-                  component={component}
-                  failedConditions={failedConditions}
-                />
-              </div>
-            )}
-
             <div className="flex-1">
               <h2 className="overview-panel-title spacer-bottom small">
                 {translate('overview.measures')}
@@ -264,7 +287,7 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {
             </div>
           </div>
         </div>
-      </div>
+      </LargeCenteredLayout>
     );
   }
 }
index 1be518dd553278e5a52c57f0585c2c27929c5da6..aba05081a7fffc3e559053c9577d242e5429e590 100644 (file)
@@ -115,7 +115,6 @@ it('should render correctly for a passed QG', async () => {
   renderPullRequestOverview({ status: 'OK', conditions: [] });
 
   expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
-  expect(screen.queryByText('overview.failed_conditions')).not.toBeInTheDocument();
 });
 
 it('should render correctly if conditions are ignored', async () => {
@@ -148,12 +147,12 @@ it('should render correctly for a failed QG', async () => {
 
   expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument();
 
-  expect(await screen.findByText('overview.failed_conditions')).toBeInTheDocument();
-
   expect(await screen.findByText('metric.new_coverage.name')).toBeInTheDocument();
   expect(await screen.findByText('quality_gates.operator.GT 2.0%')).toBeInTheDocument();
 
-  expect(await screen.findByText('metric.duplicated_lines.name')).toBeInTheDocument();
+  expect(
+    await screen.findByText('metric.duplicated_lines.name quality_gates.conditions.new_code')
+  ).toBeInTheDocument();
   expect(await screen.findByText('quality_gates.operator.GT 1.0%')).toBeInTheDocument();
 
   expect(screen.getByText('quality_gates.operator.GT 3')).toBeInTheDocument();
index 11cefef0cf95637abb26aa1092a52d72c79b06ba..fcb69fe9ab065fd5f601c99d71100deb23cdf248 100644 (file)
   border: 1px solid var(--barBorderColor);
 }
 
-.overview-quality-gate-sonar-lint-info {
-  padding: 8px 16px;
-  border: 1px solid var(--barBorderColor);
-}
-
 .overview-panel-title {
   text-transform: uppercase;
   font-weight: 600;
   background: var(--veryLightGreen);
 }
 
-/*
- * Quality Gate
- */
-
-.overview-quality-gate-badge-large {
-  padding: calc(2 * var(--gridSize));
-  color: white;
-  box-sizing: border-box;
-}
-
-.overview-quality-gate-badge-large.failed {
-  background: var(--error700);
-}
-
-.overview-quality-gate-badge-large.success {
-  background: var(--success500);
-  height: 160px;
-}
-
-.overview-quality-gate-badge-large .h3 {
-  color: white;
-}
-
 .overview-quality-gate-conditions-list {
   background-color: white;
 }
 
-.overview-quality-gate-conditions-project-name {
-  padding: calc(2 * var(--gridSize)) 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize));
-  font-size: var(--bigFontSize);
-}
-
-.overview-quality-gate-conditions-section-title {
-  border-bottom: 1px solid var(--barBorderColor);
-  margin: 0;
-  font-size: var(--baseFontSize);
-  background: var(--barBorderColor);
-}
-
-.overview-quality-gate-conditions-list-collapse {
-  margin: calc(2 * var(--gridSize)) 0;
-}
-
 .overview-quality-gate-condition,
 .overview-quality-gate-condition:hover {
   display: block;
   background-color: var(--rowHoverHighlight);
 }
 
-.overview-quality-gate-condition-container {
-  padding: calc(1.5 * var(--gridSize)) var(--gridSize) calc(1.5 * var(--gridSize))
-    calc(3 * var(--gridSize));
-  border-bottom: 1px solid var(--barBorderColor);
-}
-
-.overview-quality-gate-condition-value {
-  flex: 0 0 20%;
-  line-height: 1;
-  font-size: var(--bigFontSize);
-}
-
 /*
  * Animations
  */
   max-width: 1260px;
 }
 
-.pr-overview-failed-conditions {
-  flex: 0 0 240px;
-}
-
 .pr-overview .overview-quality-gate-condition:first-of-type {
   margin-top: 0;
 }
   border-color: var(--orange);
 }
 
-.pr-overview .overview-quality-gate-condition:hover .overview-quality-gate-condition-container,
-.pr-overview .overview-quality-gate-condition:focus .overview-quality-gate-condition-container {
-  border-color: inherit;
-}
-
-.pr-overview .overview-quality-gate-condition-metric,
-.pr-overview .overview-quality-gate-condition-period {
-  display: block;
-  max-width: 125px;
-  line-height: 16px;
-  font-size: var(--smallFontSize);
-}
-
-.pr-overview .overview-quality-gate-condition-container {
-  min-width: 150px;
-  /* three lines by 16px and 4px margin */
-  min-height: 52px;
-  padding: var(--gridSize);
-  border-top: 1px solid var(--barBorderColor);
-  border-right: 1px solid var(--barBorderColor);
-  transition: border-color 0.3s ease;
-}
-
-.pr-overview .overview-quality-gate-condition-value {
-  font-size: var(--hugeFontSize);
-}
-
-.pr-overview .overview-quality-gate-badge-large {
-  width: 240px;
-  min-height: 160px;
-  color: var(--transparentWhite);
-}
-
-.pr-overview .overview-quality-gate-sonar-lint-info {
-  width: 207px;
-}
-
 .pr-pverview .overview-measures-row {
   min-height: 85px;
 }
index bd14e1421036663cd03d42da9a65bf1d5c7b2fb2..a980441cf9ef94599995c9f8b5bdd7c039b702b3 100644 (file)
@@ -31,6 +31,7 @@ interface Props {
   metricType: string;
   small?: boolean;
   value: string | undefined;
+  ratingComponent?: JSX.Element;
 }
 
 export default function Measure({
@@ -40,6 +41,7 @@ export default function Measure({
   metricType,
   small,
   value,
+  ratingComponent,
 }: Props) {
   if (value === undefined) {
     return <span className={className}>–</span>;
@@ -58,7 +60,7 @@ export default function Measure({
   }
 
   const tooltip = <RatingTooltipContent metricKey={metricKey} value={value} />;
-  const rating = <Rating value={value} />;
+  const rating = ratingComponent || <Rating value={value} />;
 
   if (tooltip) {
     return (
diff --git a/server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx b/server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx
new file mode 100644 (file)
index 0000000..945614d
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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 { CoverageIndicator, DuplicationsIndicator, MetricsRatingBadge } from 'design-system';
+import * as React from 'react';
+import { formatMeasure } from '../../helpers/measures';
+import { MetricKey, MetricType } from '../../types/metrics';
+import Measure from './Measure';
+import { duplicationRatingConverter } from './utils';
+
+interface Props {
+  className?: string;
+  decimals?: number | null;
+  metricKey: string;
+  metricType: string;
+  small?: boolean;
+  value: string | undefined;
+}
+
+enum MetricsEnum {
+  A = 'A',
+  B = 'B',
+  C = 'C',
+  D = 'D',
+  E = 'E',
+}
+
+export default function MeasureIndicator(props: Props) {
+  const { className, metricKey, metricType, value } = props;
+
+  if (
+    metricType === MetricType.Percent &&
+    (metricKey === MetricKey.duplicated_lines_density ||
+      metricKey === MetricKey.new_duplicated_lines_density)
+  ) {
+    return (
+      <div className={className}>
+        <DuplicationsIndicator rating={duplicationRatingConverter(Number(value))} />
+      </div>
+    );
+  }
+
+  if (metricType === MetricType.Percent) {
+    return (
+      <div className={className}>
+        <CoverageIndicator value={value} />
+      </div>
+    );
+  }
+
+  const ratingFormatted = formatMeasure(value, MetricType.Rating);
+  const ratingComponent = (
+    <MetricsRatingBadge rating={ratingFormatted as MetricsEnum} label={ratingFormatted} />
+  );
+
+  return <Measure {...props} ratingComponent={ratingComponent} />;
+}
diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx b/server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx
new file mode 100644 (file)
index 0000000..e135ad3
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import * as React from 'react';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import MeasureIndicator from '../MeasureIndicator';
+
+it('renders correctly for coverage', () => {
+  render(
+    <MeasureIndicator metricKey={MetricKey.coverage} metricType={MetricType.Percent} value="73.0" />
+  );
+  expect(screen.getByRole('img')).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap b/server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap
new file mode 100644 (file)
index 0000000..41fcb07
--- /dev/null
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly for coverage 1`] = `
+<svg
+  class="donut-chart"
+  height="24"
+  role="img"
+  width="24"
+>
+  <g
+    transform="translate(0, 0)"
+  >
+    <g
+      transform="translate(12, 12)"
+    >
+      <path
+        d="M0.75,-11.977A12,12,0,1,1,-11.672,2.785L-8.709,2.271A9,9,0,1,0,0.75,-8.969Z"
+        style="fill: rgb(18,183,106);"
+      />
+      <path
+        d="M-11.929,1.307A12,12,0,0,1,-0.75,-11.977L-0.75,-8.969A9,9,0,0,0,-8.965,0.793Z"
+        style="fill: rgb(180,35,24);"
+      />
+    </g>
+  </g>
+</svg>
+`;
diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx b/server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx
new file mode 100644 (file)
index 0000000..0428864
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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 { duplicationRatingConverter } from '../utils';
+
+describe('duplicationRatingConverter', () => {
+  it('should work correctly for different use cases', () => {
+    expect(duplicationRatingConverter(-10)).toEqual('A');
+    expect(duplicationRatingConverter(2)).toEqual('A');
+    expect(duplicationRatingConverter(4)).toEqual('B');
+    expect(duplicationRatingConverter(8)).toEqual('C');
+    expect(duplicationRatingConverter(18)).toEqual('D');
+    expect(duplicationRatingConverter(20)).toEqual('E');
+    expect(duplicationRatingConverter(25)).toEqual('E');
+  });
+});
index a8af1a0a3118f197424de187cb71484630a1183e..190c79b7976f08f1250388102ed81c6ca424c68d 100644 (file)
@@ -38,3 +38,22 @@ export function enhanceMeasure(measure: Measure, metrics: Dict<Metric>): Measure
 export function getLeakValue(measure: MeasureIntern | undefined): string | undefined {
   return measure?.period?.value;
 }
+
+export function duplicationRatingConverter(val: number) {
+  const value = val || 0;
+  const THRESHOLD_A = 3;
+  const THRESHOLD_B = 5;
+  const THRESHOLD_C = 10;
+  const THRESHOLD_D = 20;
+
+  if (value < THRESHOLD_A) {
+    return 'A';
+  } else if (value < THRESHOLD_B) {
+    return 'B';
+  } else if (value < THRESHOLD_C) {
+    return 'C';
+  } else if (value < THRESHOLD_D) {
+    return 'D';
+  }
+  return 'E';
+}
index e20174318e0e10dc22fc72b7e6720f77ad9aa6df..4896bd63304a1cccaacbe656d1d560b546aceeb3 100644 (file)
@@ -21,57 +21,8 @@ import * as React from 'react';
 import { getBranchLikeQuery } from '../../helpers/branch-like';
 import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls';
 import { BranchLike } from '../../types/branch-like';
-import { MetricKey } from '../../types/metrics';
-import { Dict } from '../../types/types';
 import Link from '../common/Link';
-
-const ISSUE_MEASURES = [
-  MetricKey.violations,
-  MetricKey.new_violations,
-  MetricKey.blocker_violations,
-  MetricKey.critical_violations,
-  MetricKey.major_violations,
-  MetricKey.minor_violations,
-  MetricKey.info_violations,
-  MetricKey.new_blocker_violations,
-  MetricKey.new_critical_violations,
-  MetricKey.new_major_violations,
-  MetricKey.new_minor_violations,
-  MetricKey.new_info_violations,
-  MetricKey.open_issues,
-  MetricKey.reopened_issues,
-  MetricKey.confirmed_issues,
-  MetricKey.false_positive_issues,
-  MetricKey.code_smells,
-  MetricKey.new_code_smells,
-  MetricKey.bugs,
-  MetricKey.new_bugs,
-  MetricKey.vulnerabilities,
-  MetricKey.new_vulnerabilities,
-];
-
-const issueParamsPerMetric: Dict<Dict<string>> = {
-  [MetricKey.blocker_violations]: { resolved: 'false', severities: 'BLOCKER' },
-  [MetricKey.new_blocker_violations]: { resolved: 'false', severities: 'BLOCKER' },
-  [MetricKey.critical_violations]: { resolved: 'false', severities: 'CRITICAL' },
-  [MetricKey.new_critical_violations]: { resolved: 'false', severities: 'CRITICAL' },
-  [MetricKey.major_violations]: { resolved: 'false', severities: 'MAJOR' },
-  [MetricKey.new_major_violations]: { resolved: 'false', severities: 'MAJOR' },
-  [MetricKey.minor_violations]: { resolved: 'false', severities: 'MINOR' },
-  [MetricKey.new_minor_violations]: { resolved: 'false', severities: 'MINOR' },
-  [MetricKey.info_violations]: { resolved: 'false', severities: 'INFO' },
-  [MetricKey.new_info_violations]: { resolved: 'false', severities: 'INFO' },
-  [MetricKey.open_issues]: { resolved: 'false', statuses: 'OPEN' },
-  [MetricKey.reopened_issues]: { resolved: 'false', statuses: 'REOPENED' },
-  [MetricKey.confirmed_issues]: { resolved: 'false', statuses: 'CONFIRMED' },
-  [MetricKey.false_positive_issues]: { resolutions: 'FALSE-POSITIVE' },
-  [MetricKey.code_smells]: { resolved: 'false', types: 'CODE_SMELL' },
-  [MetricKey.new_code_smells]: { resolved: 'false', types: 'CODE_SMELL' },
-  [MetricKey.bugs]: { resolved: 'false', types: 'BUG' },
-  [MetricKey.new_bugs]: { resolved: 'false', types: 'BUG' },
-  [MetricKey.vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' },
-  [MetricKey.new_vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' },
-};
+import { isIssueMeasure, propsToIssueParams } from './utils';
 
 interface Props {
   ariaLabel?: string;
@@ -84,27 +35,12 @@ interface Props {
 }
 
 export default class DrilldownLink extends React.PureComponent<Props> {
-  isIssueMeasure = () => {
-    return ISSUE_MEASURES.indexOf(this.props.metric as MetricKey) !== -1;
-  };
-
-  propsToIssueParams = () => {
-    const params: Dict<string | boolean> = {
-      ...(issueParamsPerMetric[this.props.metric] || { resolved: 'false' }),
-    };
-
-    if (this.props.inNewCodePeriod) {
-      params.inNewCodePeriod = true;
-    }
-
-    return params;
-  };
-
   renderIssuesLink = () => {
-    const { ariaLabel, className, component, children, branchLike } = this.props;
+    const { ariaLabel, className, component, children, branchLike, metric, inNewCodePeriod } =
+      this.props;
 
     const url = getComponentIssuesUrl(component, {
-      ...this.propsToIssueParams(),
+      ...propsToIssueParams(metric, inNewCodePeriod),
       ...getBranchLikeQuery(branchLike),
     });
 
@@ -116,10 +52,11 @@ export default class DrilldownLink extends React.PureComponent<Props> {
   };
 
   render() {
-    if (this.isIssueMeasure()) {
+    const { ariaLabel, className, metric, component, children, branchLike } = this.props;
+
+    if (isIssueMeasure(metric)) {
       return this.renderIssuesLink();
     }
-    const { ariaLabel, className, metric, component, children, branchLike } = this.props;
 
     const url = getComponentDrilldownUrl({
       componentKey: component,
@@ -127,6 +64,7 @@ export default class DrilldownLink extends React.PureComponent<Props> {
       branchLike,
       listView: true,
     });
+
     return (
       <Link aria-label={ariaLabel} className={className} to={url}>
         {children}
index 871b0d9d8207125c1bc025f766e070cf26bd69d5..d1b1ea0b78742e62296ebb1c55a4e9794ce0ec3d 100644 (file)
@@ -30,21 +30,6 @@ it('should render issuesLink correctly', () => {
   expect(wrapper).toMatchSnapshot();
 });
 
-describe('propsToIssueParams', () => {
-  it('should render correct default parameters', () => {
-    const wrapper = shallowRender();
-    expect(wrapper.instance().propsToIssueParams()).toEqual({ resolved: 'false' });
-  });
-
-  it(`should render correct params`, () => {
-    const wrapper = shallowRender({ metric: 'false_positive_issues', inNewCodePeriod: true });
-    expect(wrapper.instance().propsToIssueParams()).toEqual({
-      resolutions: 'FALSE-POSITIVE',
-      inNewCodePeriod: true,
-    });
-  });
-});
-
 const shallowRender = (props: Partial<DrilldownLink['props']> = {}, label = 'label') => {
   return shallow<DrilldownLink>(
     <DrilldownLink component="project123" metric="other" {...props}>
diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts b/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts
new file mode 100644 (file)
index 0000000..4952486
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 { MetricKey } from '../../../types/metrics';
+import { propsToIssueParams } from '../utils';
+
+describe('propsToIssueParams', () => {
+  it('should render correct default parameters', () => {
+    expect(propsToIssueParams('other')).toEqual({ resolved: 'false' });
+  });
+
+  it(`should render correct params`, () => {
+    expect(propsToIssueParams(MetricKey.false_positive_issues, true)).toEqual({
+      resolutions: 'FALSE-POSITIVE',
+      inNewCodePeriod: true,
+    });
+  });
+});
diff --git a/server/sonar-web/src/main/js/components/shared/utils.ts b/server/sonar-web/src/main/js/components/shared/utils.ts
new file mode 100644 (file)
index 0000000..a073929
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { MetricKey } from '../../types/metrics';
+import { Dict } from '../../types/types';
+
+const ISSUE_MEASURES = [
+  MetricKey.violations,
+  MetricKey.new_violations,
+  MetricKey.blocker_violations,
+  MetricKey.critical_violations,
+  MetricKey.major_violations,
+  MetricKey.minor_violations,
+  MetricKey.info_violations,
+  MetricKey.new_blocker_violations,
+  MetricKey.new_critical_violations,
+  MetricKey.new_major_violations,
+  MetricKey.new_minor_violations,
+  MetricKey.new_info_violations,
+  MetricKey.open_issues,
+  MetricKey.reopened_issues,
+  MetricKey.confirmed_issues,
+  MetricKey.false_positive_issues,
+  MetricKey.code_smells,
+  MetricKey.new_code_smells,
+  MetricKey.bugs,
+  MetricKey.new_bugs,
+  MetricKey.vulnerabilities,
+  MetricKey.new_vulnerabilities,
+];
+
+const issueParamsPerMetric: Dict<Dict<string>> = {
+  [MetricKey.blocker_violations]: { resolved: 'false', severities: 'BLOCKER' },
+  [MetricKey.new_blocker_violations]: { resolved: 'false', severities: 'BLOCKER' },
+  [MetricKey.critical_violations]: { resolved: 'false', severities: 'CRITICAL' },
+  [MetricKey.new_critical_violations]: { resolved: 'false', severities: 'CRITICAL' },
+  [MetricKey.major_violations]: { resolved: 'false', severities: 'MAJOR' },
+  [MetricKey.new_major_violations]: { resolved: 'false', severities: 'MAJOR' },
+  [MetricKey.minor_violations]: { resolved: 'false', severities: 'MINOR' },
+  [MetricKey.new_minor_violations]: { resolved: 'false', severities: 'MINOR' },
+  [MetricKey.info_violations]: { resolved: 'false', severities: 'INFO' },
+  [MetricKey.new_info_violations]: { resolved: 'false', severities: 'INFO' },
+  [MetricKey.open_issues]: { resolved: 'false', statuses: 'OPEN' },
+  [MetricKey.reopened_issues]: { resolved: 'false', statuses: 'REOPENED' },
+  [MetricKey.confirmed_issues]: { resolved: 'false', statuses: 'CONFIRMED' },
+  [MetricKey.false_positive_issues]: { resolutions: 'FALSE-POSITIVE' },
+  [MetricKey.code_smells]: { resolved: 'false', types: 'CODE_SMELL' },
+  [MetricKey.new_code_smells]: { resolved: 'false', types: 'CODE_SMELL' },
+  [MetricKey.bugs]: { resolved: 'false', types: 'BUG' },
+  [MetricKey.new_bugs]: { resolved: 'false', types: 'BUG' },
+  [MetricKey.vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' },
+  [MetricKey.new_vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' },
+};
+
+export function isIssueMeasure(metric: string) {
+  return ISSUE_MEASURES.indexOf(metric as MetricKey) !== -1;
+}
+
+export function propsToIssueParams(metric: string, inNewCodePeriod = false) {
+  const params: Dict<string | boolean> = {
+    ...(issueParamsPerMetric[metric] || { resolved: 'false' }),
+  };
+
+  if (inNewCodePeriod) {
+    params.inNewCodePeriod = true;
+  }
+
+  return params;
+}
index d2e143c4e27ff9ef0baf6b67e8dfd74820331f2f..386ab30be8b07c28f09e49154b764b273e5dfb43 100644 (file)
@@ -3370,17 +3370,15 @@ system.version_is_availble={version} is available
 # OVERVIEW
 #
 #------------------------------------------------------------------------------
-overview.failed_conditions=Failed conditions
-overview.X_more_failed_conditions={0} more failed conditions
-overview.1_condition_failed=1 condition failed
-overview.X_conditions_failed={0} conditions failed
+overview.X_conditions_failed={0} failed condition(s)
 overview.fix_failed_conditions_with_sonarlint=Fix issues before they fail your Quality Gate with {link} in your IDE. Power up with connected mode!
-overview.quality_gate=Quality Gate Status
+overview.quality_gate.status=Quality Gate Status
+overview.quality_gate=Quality Gate
 overview.quality_gate_x=Quality Gate: {0}
 overview.quality_gate.help=A Quality Gate is a set of measure-based Boolean conditions. It helps you know immediately whether your project is production-ready. If your current status is not Passed, you'll see which measures caused the problem and the values required to pass.
 overview.quality_gate_failed_with_x=with {0} errors
 overview.quality_gate_code_clean=Your code is clean!
-overview.quality_gate_all_conditions_passed=All conditions passed.
+overview.passed.clean_code=Enjoy your sparkling clean code!
 overview.you_should_define_quality_gate=You should define a quality gate on this project.
 overview.quality_gate.ignored_conditions=Some Quality Gate conditions on New Code were ignored because of the small number of New Lines
 overview.quality_gate.ignored_conditions.tooltip=At the start of a new code period, if very few lines have been added or modified, it might be difficult to reach the desired level of code coverage or duplications. To prevent Quality Gate failure when there's little that can be done about it, Quality Gate conditions about duplications in new code and coverage on new code are ignored until the number of new lines is at least 20. An administrator can disable this in the general settings.