]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20337 Migrating quality gate condition list to adopt new UI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Tue, 5 Sep 2023 10:56:28 +0000 (12:56 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 19 Sep 2023 20:02:46 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/Card.tsx
server/sonar-web/design-system/src/components/Table.tsx
server/sonar-web/design-system/src/components/Title.tsx
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx

index 4ec5254342f1c59301ad32d1cd0138c077fa525f..66ead225344938b304f566cf7e2f95692d5074fe 100644 (file)
@@ -38,6 +38,10 @@ export function GreyCard(props: CardProps) {
   return <GreyCardStyled {...rest}>{children}</GreyCardStyled>;
 }
 
+export const CardWithPrimaryBackground = styled(Card)`
+  background-color: ${themeColor('backgroundPrimary')};
+`;
+
 const CardStyled = styled.div`
   background-color: ${themeColor('backgroundSecondary')};
   border: ${themeBorder('default', 'projectCardBorder')};
index b97b2237fc60f6202685bfcb877406807108d690..b44ce861b8bb4ce7c6eaf064ce4a4572615e4bca 100644 (file)
@@ -188,6 +188,14 @@ export function RatingCell({ children, ...props }: CellComponentProps) {
   );
 }
 
+export function ActionCell({ children, ...props }: CellComponentProps) {
+  return (
+    <CellComponent {...props}>
+      <div className="sw-text-right sw-justify-end sw-flex sw-items-center">{children}</div>
+    </CellComponent>
+  );
+}
+
 export function CheckboxCell({ children, ...props }: CellComponentProps) {
   return (
     <CellComponent {...props}>
index 3e4b8d26969068d55d60e17286932bdbb4a739f5..c3acb2d9ed12b26a8e90041de7479a95ef0e8032 100644 (file)
@@ -33,6 +33,12 @@ export const SubTitle = styled.h2`
   color: ${themeColor('pageTitle')};
 `;
 
+export const HeadingDark = styled.h2`
+  color: ${themeColor('pageContentDark')};
+
+  ${tw`sw-body-sm-highlight`}
+`;
+
 export const SubHeading = styled.h3`
   ${tw`sw-body-md-highlight`}
   ${tw`sw-mb-2`}
index d58b820d48935f8e5cde59e4e784b44629692908..3b4fafbc2a83c09d9e8d6eed3da554203d0eca85 100644 (file)
@@ -415,6 +415,10 @@ export const lightTheme = {
     qgIndicatorFailed: COLORS.red[200],
     qgIndicatorNotComputed: COLORS.blueGrey[200],
 
+    // quality gate texts colors
+    qgConditionNotCayc: COLORS.red[600],
+    qgConditionCayc: COLORS.green[600],
+
     // main bar
     mainBar: COLORS.white,
     mainBarHover: COLORS.blueGrey[600],
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx
new file mode 100644 (file)
index 0000000..5dc0018
--- /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 classNames from 'classnames';
+import { CheckIcon, LightLabel } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default function CaycConditionsListItem({ index, last }: { index: number; last: boolean }) {
+  return (
+    <li className={classNames('sw-flex', { 'sw-mb-2': !last })}>
+      <CheckIcon className="sw-mr-1 sw-pt-1/2" />
+      <LightLabel>{translate(`quality_gates.cayc.banner.list_item${index + 1}`)}</LightLabel>
+    </li>
+  );
+}
index 2c3e8117c06629f9d03561ef1827b9a167c5cc17..5623df940983dfa62cd6141a9ca03a62d7d0f526 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import classNames from 'classnames';
+import {
+  ActionCell,
+  ContentCell,
+  DestructiveIcon,
+  InteractiveIcon,
+  NumericalCell,
+  PencilIcon,
+  TableRow,
+  TextError,
+  TrashIcon,
+} from 'design-system';
 import * as React from 'react';
 import { deleteCondition } from '../../../api/quality-gates';
 import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
-import { DeleteButton, EditButton } from '../../../components/controls/buttons';
 import ConfirmModal from '../../../components/controls/ConfirmModal';
 import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
 import {
@@ -116,39 +126,39 @@ export class ConditionComponent extends React.PureComponent<Props, State> {
     const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant;
 
     return (
-      <tr className={classNames({ highlighted: updated })}>
-        <td className="text-middle">
+      <TableRow className={classNames({ highlighted: updated })}>
+        <ContentCell>
           {getLocalizedMetricNameNoDiffMetric(metric, metrics)}
-          {metric.hidden && (
-            <span className="text-danger little-spacer-left">{translate('deprecated')}</span>
-          )}
-        </td>
+          {metric.hidden && <TextError className="sw-ml-1" text={translate('deprecated')} />}
+        </ContentCell>
 
-        <td className="text-middle nowrap">{this.renderOperator()}</td>
+        <ContentCell className="sw-whitespace-nowrap">{this.renderOperator()}</ContentCell>
 
-        <td className="text-middle nowrap">
+        <NumericalCell className="sw-whitespace-nowrap">
           <ConditionValue
             metric={metric}
             isCaycModal={isCaycModal}
             condition={condition}
             isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant}
           />
-        </td>
-        <td className="text-middle nowrap display-flex-justify-end">
+        </NumericalCell>
+        <ActionCell>
           {!isCaycModal && canEdit && (
             <>
               {(!isCaycCompliantAndOverCompliant ||
                 !CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric) ||
                 (isCaycCompliantAndOverCompliant && showEdit)) && (
                 <>
-                  <EditButton
+                  <InteractiveIcon
+                    Icon={PencilIcon}
                     aria-label={translateWithParameters(
                       'quality_gates.condition.edit',
                       metric.name,
                     )}
                     data-test="quality-gates__condition-update"
                     onClick={this.handleOpenUpdate}
-                    className="spacer-right"
+                    className="sw-mr-4"
+                    size="small"
                   />
                   {this.state.modal && (
                     <ConditionModal
@@ -166,13 +176,15 @@ export class ConditionComponent extends React.PureComponent<Props, State> {
                 !isCaycCondition(condition) ||
                 (isCaycCompliantAndOverCompliant && showEdit)) && (
                 <>
-                  <DeleteButton
+                  <DestructiveIcon
+                    Icon={TrashIcon}
                     aria-label={translateWithParameters(
                       'quality_gates.condition.delete',
                       metric.name,
                     )}
                     data-test="quality-gates__condition-delete"
                     onClick={this.handleDeleteClick}
+                    size="small"
                   />
                   {this.state.deleteFormOpen && (
                     <ConfirmModal
@@ -193,8 +205,8 @@ export class ConditionComponent extends React.PureComponent<Props, State> {
               )}
             </>
           )}
-        </td>
-      </tr>
+        </ActionCell>
+      </TableRow>
     );
   }
 }
index 92191f4d81962888dee1c62f2f0751cb8ec03133..5845f1ca957f77205effcfa9988f301dc238aecc 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 styled from '@emotion/styled';
 import classNames from 'classnames';
+import { themeColor } from 'design-system';
 import * as React from 'react';
 import { formatMeasure } from '../../../helpers/measures';
 import { Condition, Metric } from '../../../types/types';
@@ -43,15 +46,15 @@ function ConditionValue({
     return (
       <>
         {isToBeModified && (
-          <span className="red-text strike-through spacer-right">
+          <RedColorText className="sw-line-through sw-mr-2">
             {formatMeasure(condition.error, metric.type)}
-          </span>
+          </RedColorText>
         )}
-        <span className={classNames('spacer-right', { 'green-text': isToBeModified })}>
+        <GreenColorText isToBeModified={isToBeModified} className={classNames('sw-mr-2')}>
           {formatMeasure(getCorrectCaycCondition(condition).error, metric.type)}
-        </span>
+        </GreenColorText>
         <ConditionValueDescription
-          className={classNames({ 'green-text': isToBeModified })}
+          isToBeModified={isToBeModified}
           condition={getCorrectCaycCondition(condition)}
           metric={metric}
         />
@@ -61,7 +64,7 @@ function ConditionValue({
 
   return (
     <>
-      <span className="spacer-right">{formatMeasure(condition.error, metric.type)}</span>
+      <span className="sw-mr-2">{formatMeasure(condition.error, metric.type)}</span>
       {isCaycCompliantAndOverCompliant && isCaycCondition(condition) && (
         <ConditionValueDescription condition={condition} metric={metric} />
       )}
@@ -70,3 +73,11 @@ function ConditionValue({
 }
 
 export default ConditionValue;
+
+const RedColorText = styled.span`
+  color: ${themeColor('qgConditionNotCayc')};
+`;
+
+export const GreenColorText = styled.span<{ isToBeModified: boolean }>`
+  color: ${(props) => (props.isToBeModified ? themeColor('qgConditionCayc') : 'inherit')};
+`;
index 1f330c1676734dab543b8edb502fb81092eeee41..190f3bd885768a4f8535872cc4b2ad7a7d38b454 100644 (file)
@@ -31,6 +31,7 @@ import { MetricKey, MetricType } from '../../../types/metrics';
 import { GlobalSettingKeys } from '../../../types/settings';
 import { Condition, Metric } from '../../../types/types';
 import { isCaycCondition } from '../utils';
+import { GreenColorText } from './ConditionValue';
 
 const NO_DESCRIPTION_CONDITION = [
   'new_security_hotspots_reviewed',
@@ -42,14 +43,14 @@ interface Props {
   appState: AppState;
   condition: Condition;
   metric: Metric;
-  className?: string;
+  isToBeModified?: boolean;
 }
 
 function ConditionValueDescription({
   condition,
   appState: { settings },
   metric,
-  className = '',
+  isToBeModified = false,
 }: Props) {
   if (condition.metric === MetricKey.new_maintainability_rating) {
     const maintainabilityGrid = getMaintainabilityGrid(
@@ -60,7 +61,7 @@ function ConditionValueDescription({
     const ratingLetter = formatMeasure(condition.error, MetricType.Rating);
 
     return (
-      <span className={className}>
+      <GreenColorText isToBeModified={isToBeModified}>
         (
         {condition.error === '1'
           ? translateWithParameters(
@@ -76,12 +77,12 @@ function ConditionValueDescription({
               ),
             )}
         )
-      </span>
+      </GreenColorText>
     );
   }
 
   return (
-    <span className={className}>
+    <GreenColorText isToBeModified={isToBeModified}>
       {isCaycCondition(condition) && !NO_DESCRIPTION_CONDITION.includes(condition.metric) && (
         <>
           (
@@ -91,7 +92,7 @@ function ConditionValueDescription({
           )
         </>
       )}
-    </span>
+    </GreenColorText>
   );
 }
 
index d4b4895b5d33bd26e72faacec93994bffaeb3950..69e0f2237e626e527eb1e91b7261b8958fc7cdb8 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 { differenceWith, map, uniqBy } from 'lodash';
+
+import {
+  ButtonSecondary,
+  CardWithPrimaryBackground,
+  FlagMessage,
+  HeadingDark,
+  HelperHintIcon,
+  LightPrimary,
+  Link,
+  SubHeading,
+  SubnavigationFlowSeparator,
+  Title,
+} from 'design-system';
+import { differenceWith, map, times, uniqBy } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import withAvailableFeatures, {
   WithAvailableFeaturesProps,
 } from '../../../app/components/available-features/withAvailableFeatures';
 import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
-import DocLink from '../../../components/common/DocLink';
 import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
 import ModalButton, { ModalProps } from '../../../components/controls/ModalButton';
-import { Button } from '../../../components/controls/buttons';
-import { Alert } from '../../../components/ui/Alert';
+import { useDocUrl } from '../../../helpers/docs';
 import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
 import { Feature } from '../../../types/features';
 import { MetricKey } from '../../../types/metrics';
@@ -40,10 +51,13 @@ import {
   QualityGate,
 } from '../../../types/types';
 import { groupAndSortByPriorityConditions } from '../utils';
+import CaycConditionsListItem from './CaycConditionsListItem';
 import ConditionModal from './ConditionModal';
 import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal';
 import ConditionsTable from './ConditionsTable';
 
+export const CAYC_CONDITIONS_LIST_ITEMS = 6;
+
 interface Props extends WithAvailableFeaturesProps {
   metrics: Dict<Metric>;
   onAddCondition: (condition: ConditionType) => void;
@@ -53,10 +67,6 @@ interface Props extends WithAvailableFeaturesProps {
   updatedConditionId?: string;
 }
 
-interface State {
-  unlockEditing: boolean;
-}
-
 const FORBIDDEN_METRIC_TYPES = ['DATA', 'DISTRIB', 'STRING', 'BOOL'];
 const FORBIDDEN_METRICS: string[] = [
   MetricKey.alert_status,
@@ -65,181 +75,166 @@ const FORBIDDEN_METRICS: string[] = [
   MetricKey.new_security_hotspots,
 ];
 
-export class Conditions extends React.PureComponent<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      unlockEditing: props.qualityGate.caycStatus === CaycStatus.NonCompliant,
-    };
-  }
+export function Conditions({
+  qualityGate,
+  metrics,
+  onRemoveCondition,
+  onSaveCondition,
+  onAddCondition,
+  hasFeature,
+  updatedConditionId,
+}: Props) {
+  const [editing, setEditing] = React.useState<boolean>(
+    qualityGate.caycStatus === CaycStatus.NonCompliant
+  );
+  const canEdit = Boolean(qualityGate.actions?.manageConditions);
+  const { conditions = [] } = qualityGate;
+  const existingConditions = conditions.filter((condition) => metrics[condition.metric]);
+  const { overallCodeConditions, newCodeConditions } = groupAndSortByPriorityConditions(
+    existingConditions,
+    metrics
+  );
 
-  componentDidUpdate(prevProps: Readonly<Props>): void {
-    const { qualityGate } = this.props;
-    if (prevProps.qualityGate.name !== qualityGate.name) {
-      this.setState({ unlockEditing: qualityGate.caycStatus === CaycStatus.NonCompliant });
+  const duplicates: ConditionType[] = [];
+  const savedConditions = existingConditions.filter((condition) => condition.id != null);
+  savedConditions.forEach((condition) => {
+    const sameCount = savedConditions.filter((sample) => sample.metric === condition.metric).length;
+    if (sameCount > 1) {
+      duplicates.push(condition);
     }
-  }
-
-  unlockEditing = () => {
-    this.setState({ unlockEditing: true });
-  };
+  });
 
-  lockEditing = () => {
-    this.setState({ unlockEditing: false });
-  };
+  const uniqDuplicates = uniqBy(duplicates, (d) => d.metric).map((condition) => ({
+    ...condition,
+    metric: metrics[condition.metric],
+  }));
 
-  renderConditionModal = ({ onClose }: ModalProps) => {
-    const { metrics, qualityGate } = this.props;
-    const { conditions = [] } = qualityGate;
-    const availableMetrics = differenceWith(
-      map(metrics, (metric) => metric).filter(
-        (metric) =>
-          !metric.hidden &&
-          !FORBIDDEN_METRIC_TYPES.includes(metric.type) &&
-          !FORBIDDEN_METRICS.includes(metric.key),
-      ),
-      conditions,
-      (metric, condition) => metric.key === condition.metric,
-    );
-    return (
-      <ConditionModal
-        header={translate('quality_gates.add_condition')}
-        metrics={availableMetrics}
-        onAddCondition={this.props.onAddCondition}
-        onClose={onClose}
-        qualityGate={qualityGate}
-      />
-    );
-  };
+  const getDocUrl = useDocUrl();
 
-  renderCaycModal = ({ onClose }: ModalProps) => {
-    const { qualityGate, metrics } = this.props;
-    const { conditions = [] } = qualityGate;
-    const canEdit = Boolean(qualityGate.actions?.manageConditions);
-    return (
-      <CaycReviewUpdateConditionsModal
-        qualityGate={qualityGate}
-        metrics={metrics}
-        canEdit={canEdit}
-        onRemoveCondition={this.props.onRemoveCondition}
-        onSaveCondition={this.props.onSaveCondition}
-        onAddCondition={this.props.onAddCondition}
-        lockEditing={this.lockEditing}
-        updatedConditionId={this.props.updatedConditionId}
-        conditions={conditions}
-        scope="new-cayc"
-        onClose={onClose}
-      />
-    );
-  };
+  React.useEffect(() => {
+    setEditing(qualityGate.caycStatus === CaycStatus.NonCompliant);
+  }, [qualityGate]);
 
-  render() {
-    const { qualityGate, metrics, onRemoveCondition, onSaveCondition, updatedConditionId } =
-      this.props;
-    const canEdit = Boolean(qualityGate.actions?.manageConditions);
-    const { unlockEditing } = this.state;
-    const { conditions = [] } = qualityGate;
-    const existingConditions = conditions.filter((condition) => metrics[condition.metric]);
-    const { overallCodeConditions, newCodeConditions } = groupAndSortByPriorityConditions(
-      existingConditions,
-      metrics,
-    );
+  const renderConditionModal = React.useCallback(
+    ({ onClose }: ModalProps) => {
+      const { conditions = [] } = qualityGate;
+      const availableMetrics = differenceWith(
+        map(metrics, (metric) => metric).filter(
+          (metric) =>
+            !metric.hidden &&
+            !FORBIDDEN_METRIC_TYPES.includes(metric.type) &&
+            !FORBIDDEN_METRICS.includes(metric.key)
+        ),
+        conditions,
+        (metric, condition) => metric.key === condition.metric
+      );
+      return (
+        <ConditionModal
+          header={translate('quality_gates.add_condition')}
+          metrics={availableMetrics}
+          onAddCondition={onAddCondition}
+          onClose={onClose}
+          qualityGate={qualityGate}
+        />
+      );
+    },
+    [metrics, qualityGate, onAddCondition]
+  );
 
-    const duplicates: ConditionType[] = [];
-    const savedConditions = existingConditions.filter((condition) => condition.id != null);
-    savedConditions.forEach((condition) => {
-      const sameCount = savedConditions.filter(
-        (sample) => sample.metric === condition.metric,
-      ).length;
-      if (sameCount > 1) {
-        duplicates.push(condition);
-      }
-    });
+  const renderCaycModal = React.useCallback(
+    ({ onClose }: ModalProps) => {
+      const { conditions = [] } = qualityGate;
+      const canEdit = Boolean(qualityGate.actions?.manageConditions);
+      return (
+        <CaycReviewUpdateConditionsModal
+          qualityGate={qualityGate}
+          metrics={metrics}
+          canEdit={canEdit}
+          onRemoveCondition={onRemoveCondition}
+          onSaveCondition={onSaveCondition}
+          onAddCondition={onAddCondition}
+          lockEditing={() => setEditing(false)}
+          updatedConditionId={updatedConditionId}
+          conditions={conditions}
+          scope="new-cayc"
+          onClose={onClose}
+        />
+      );
+    },
+    [qualityGate, metrics, updatedConditionId, onAddCondition, onRemoveCondition, onSaveCondition]
+  );
 
-    const uniqDuplicates = uniqBy(duplicates, (d) => d.metric).map((condition) => ({
-      ...condition,
-      metric: metrics[condition.metric],
-    }));
-
-    return (
-      <div className="quality-gate-section">
-        {qualityGate.caycStatus !== CaycStatus.NonCompliant && (
-          <Alert className="big-spacer-top big-spacer-bottom cayc-success-banner" variant="success">
-            <h4 className="spacer-bottom cayc-success-header">
-              {translate('quality_gates.cayc.banner.title')}
-            </h4>
-            <div className="cayc-warning-description">
-              <FormattedMessage
-                id="quality_gates.cayc.banner.description1"
-                defaultMessage={translate('quality_gates.cayc.banner.description1')}
-                values={{
-                  cayc_link: (
-                    <DocLink to="/user-guide/clean-as-you-code/">
-                      {translate('quality_gates.cayc')}
-                    </DocLink>
-                  ),
-                }}
-              />
-              <br />
-              {translate('quality_gates.cayc.banner.description2')}
-            </div>
-            <ul className="big-spacer-top big-spacer-left spacer-bottom cayc-warning-description">
-              <li>{translate('quality_gates.cayc.banner.list_item1')}</li>
-              <li>{translate('quality_gates.cayc.banner.list_item2')}</li>
-              <li>{translate('quality_gates.cayc.banner.list_item3')}</li>
-              <li>{translate('quality_gates.cayc.banner.list_item4')}</li>
-              <li>{translate('quality_gates.cayc.banner.list_item5')}</li>
-              <li>{translate('quality_gates.cayc.banner.list_item6')}</li>
-            </ul>
-          </Alert>
-        )}
-
-        {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && (
-          <Alert className="big-spacer-top big-spacer-bottom" variant="warning" role="alert">
-            <h4 className="spacer-bottom cayc-warning-header">
-              {translate('quality_gates.cayc_missing.banner.title')}
-            </h4>
-            <div className="cayc-warning-description spacer-bottom">
-              <FormattedMessage
-                id="quality_gates.cayc_missing.banner.description"
-                defaultMessage={translate('quality_gates.cayc_missing.banner.description')}
-                values={{
-                  cayc_link: (
-                    <DocLink to="/user-guide/clean-as-you-code/">
-                      {translate('quality_gates.cayc')}
-                    </DocLink>
-                  ),
-                }}
+  return (
+    <div>
+      {qualityGate.caycStatus !== CaycStatus.NonCompliant && (
+        <CardWithPrimaryBackground className="sw-mb-9 sw-p-8">
+          <Title as="h2" className="sw-mb-2 sw-heading-md">
+            {translate('quality_gates.cayc.banner.title')}
+          </Title>
+          <SubHeading className="sw-body-sm sw-mb-4">
+            <FormattedMessage
+              id="quality_gates.cayc.banner.description1"
+              defaultMessage={translate('quality_gates.cayc.banner.description1')}
+              values={{
+                cayc_link: (
+                  <Link to={getDocUrl('/user-guide/clean-as-you-code/')}>
+                    {translate('quality_gates.cayc')}
+                  </Link>
+                ),
+              }}
+            />
+            {translate('quality_gates.cayc.banner.description2')}
+          </SubHeading>
+          <ul className="sw-body-sm">
+            {times(CAYC_CONDITIONS_LIST_ITEMS, (i) => (
+              <CaycConditionsListItem
+                index={i}
+                key={i}
+                last={i === CAYC_CONDITIONS_LIST_ITEMS - 1}
               />
-            </div>
-            {canEdit && (
-              <ModalButton modal={this.renderCaycModal}>
-                {({ onClick }) => (
-                  <Button className="big-spacer-top spacer-bottom" onClick={onClick}>
-                    {translate('quality_gates.cayc_condition.review_update')}
-                  </Button>
-                )}
-              </ModalButton>
-            )}
-          </Alert>
-        )}
+            ))}
+          </ul>
+        </CardWithPrimaryBackground>
+      )}
 
-        {(qualityGate.caycStatus === CaycStatus.NonCompliant || unlockEditing) && canEdit && (
-          <div className="pull-right">
-            <ModalButton modal={this.renderConditionModal}>
+      {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && (
+        <CardWithPrimaryBackground className="sw-mb-9 sw-p-8">
+          <Title as="h2" className="sw-mb-2 sw-heading-md">
+            {translate('quality_gates.cayc_missing.banner.title')}
+          </Title>
+          <SubHeading className="sw-body-sm sw-mb-4">
+            <FormattedMessage
+              id="quality_gates.cayc_missing.banner.description"
+              defaultMessage={translate('quality_gates.cayc_missing.banner.description')}
+              values={{
+                cayc_link: (
+                  <Link to={getDocUrl('/user-guide/clean-as-you-code/')}>
+                    {translate('quality_gates.cayc')}
+                  </Link>
+                ),
+              }}
+            />
+          </SubHeading>
+          <SubnavigationFlowSeparator className="sw-m-0" />
+          {canEdit && (
+            <ModalButton modal={renderCaycModal}>
               {({ onClick }) => (
-                <Button data-test="quality-gates__add-condition" onClick={onClick}>
-                  {translate('quality_gates.add_condition')}
-                </Button>
+                <ButtonSecondary className="sw-mt-4" onClick={onClick}>
+                  {translate('quality_gates.cayc_condition.review_update')}
+                </ButtonSecondary>
               )}
             </ModalButton>
-          </div>
-        )}
+          )}
+        </CardWithPrimaryBackground>
+      )}
 
-        <header className="display-flex-center">
-          <h2 className="big">{translate('quality_gates.conditions')}</h2>
+      <header className="sw-flex sw-items-center sw-mb-4 sw-justify-between">
+        <div className="sw-flex">
+          <HeadingDark className="sw-body-md-highlight sw-m-0">
+            {translate('quality_gates.conditions')}
+          </HeadingDark>
           <DocumentationTooltip
-            className="spacer-left"
+            className="sw-ml-2"
             content={translate('quality_gates.conditions.help')}
             links={[
               {
@@ -247,96 +242,113 @@ export class Conditions extends React.PureComponent<Props, State> {
                 label: translate('quality_gates.conditions.help.link'),
               },
             ]}
-          />
-        </header>
+          >
+            <HelperHintIcon />
+          </DocumentationTooltip>
+        </div>
+        <div>
+          {(qualityGate.caycStatus === CaycStatus.NonCompliant || editing) && canEdit && (
+            <ModalButton modal={renderConditionModal}>
+              {({ onClick }) => (
+                <ButtonSecondary data-test="quality-gates__add-condition" onClick={onClick}>
+                  {translate('quality_gates.add_condition')}
+                </ButtonSecondary>
+              )}
+            </ModalButton>
+          )}
+        </div>
+      </header>
 
-        {uniqDuplicates.length > 0 && (
-          <Alert variant="warning">
+      {uniqDuplicates.length > 0 && (
+        <FlagMessage variant="warning" className="sw-flex sw-mb-4">
+          <p>
             <p>{translate('quality_gates.duplicated_conditions')}</p>
-            <ul className="list-styled spacer-top">
+            <ul className="sw-my-2 sw-list-disc sw-pl-10">
               {uniqDuplicates.map((d) => (
                 <li key={d.metric.key}>{getLocalizedMetricName(d.metric)}</li>
               ))}
             </ul>
-          </Alert>
-        )}
+          </p>
+        </FlagMessage>
+      )}
 
-        {newCodeConditions.length > 0 && (
-          <div className="big-spacer-top">
-            <h3 className="medium text-normal">
-              {translate('quality_gates.conditions.new_code', 'long')}
-            </h3>
-            {this.props.hasFeature(Feature.BranchSupport) && (
-              <p className="spacer-top spacer-bottom">
-                {translate('quality_gates.conditions.new_code', 'description')}
-              </p>
-            )}
-            <ConditionsTable
-              qualityGate={qualityGate}
-              metrics={metrics}
-              canEdit={canEdit}
-              onRemoveCondition={onRemoveCondition}
-              onSaveCondition={onSaveCondition}
-              updatedConditionId={updatedConditionId}
-              conditions={newCodeConditions}
-              showEdit={this.state.unlockEditing}
-              scope="new"
-            />
-          </div>
-        )}
+      {newCodeConditions.length > 0 && (
+        <div>
+          <HeadingDark as="h3" className="sw-mb-2">
+            {translate('quality_gates.conditions.new_code', 'long')}
+          </HeadingDark>
+          {hasFeature(Feature.BranchSupport) && (
+            <SubHeading as="p" className="sw-mb-2 sw-body-sm">
+              {translate('quality_gates.conditions.new_code', 'description')}
+            </SubHeading>
+          )}
+          <ConditionsTable
+            qualityGate={qualityGate}
+            metrics={metrics}
+            canEdit={canEdit}
+            onRemoveCondition={onRemoveCondition}
+            onSaveCondition={onSaveCondition}
+            updatedConditionId={updatedConditionId}
+            conditions={newCodeConditions}
+            showEdit={editing}
+            scope="new"
+          />
+        </div>
+      )}
 
-        {overallCodeConditions.length > 0 && (
-          <div className="big-spacer-top">
-            <h3 className="medium text-normal">
-              {translate('quality_gates.conditions.overall_code', 'long')}
-            </h3>
+      {overallCodeConditions.length > 0 && (
+        <div className="sw-mt-5">
+          <HeadingDark as="h3" className="sw-mb-2">
+            {translate('quality_gates.conditions.overall_code', 'long')}
+          </HeadingDark>
 
-            {this.props.hasFeature(Feature.BranchSupport) && (
-              <p className="spacer-top spacer-bottom">
-                {translate('quality_gates.conditions.overall_code', 'description')}
-              </p>
-            )}
+          {hasFeature(Feature.BranchSupport) && (
+            <SubHeading as="p" className="sw-mb-2 sw-body-sm">
+              {translate('quality_gates.conditions.overall_code', 'description')}
+            </SubHeading>
+          )}
 
-            <ConditionsTable
-              qualityGate={qualityGate}
-              metrics={metrics}
-              canEdit={canEdit}
-              onRemoveCondition={onRemoveCondition}
-              onSaveCondition={onSaveCondition}
-              updatedConditionId={updatedConditionId}
-              conditions={overallCodeConditions}
-              scope="overall"
-            />
-          </div>
-        )}
+          <ConditionsTable
+            qualityGate={qualityGate}
+            metrics={metrics}
+            canEdit={canEdit}
+            onRemoveCondition={onRemoveCondition}
+            onSaveCondition={onSaveCondition}
+            updatedConditionId={updatedConditionId}
+            conditions={overallCodeConditions}
+            scope="overall"
+          />
+        </div>
+      )}
 
-        {qualityGate.caycStatus !== CaycStatus.NonCompliant && !unlockEditing && canEdit && (
-          <div className="big-spacer-top big-spacer-bottom cayc-warning-description it__qg-unfollow-cayc">
-            <p>
-              <FormattedMessage
-                id="quality_gates.cayc_unfollow.description"
-                defaultMessage={translate('quality_gates.cayc_unfollow.description')}
-                values={{
-                  cayc_link: (
-                    <DocLink to="/user-guide/clean-as-you-code/">
-                      {translate('quality_gates.cayc')}
-                    </DocLink>
-                  ),
-                }}
-              />
-            </p>
-            <Button className="big-spacer-top spacer-bottom" onClick={this.unlockEditing}>
-              {translate('quality_gates.cayc.unlock_edit')}
-            </Button>
-          </div>
-        )}
+      {qualityGate.caycStatus !== CaycStatus.NonCompliant && !editing && canEdit && (
+        <div className="sw-mt-4 sw-mb-10 it__qg-unfollow-cayc">
+          <SubHeading as="p" className="sw-mb-2 sw-body-sm">
+            <FormattedMessage
+              id="quality_gates.cayc_unfollow.description"
+              defaultMessage={translate('quality_gates.cayc_unfollow.description')}
+              values={{
+                cayc_link: (
+                  <Link to={getDocUrl('/user-guide/clean-as-you-code/')}>
+                    {translate('quality_gates.cayc')}
+                  </Link>
+                ),
+              }}
+            />
+          </SubHeading>
+          <ButtonSecondary className="sw-mt-2" onClick={() => setEditing(true)}>
+            {translate('quality_gates.cayc.unlock_edit')}
+          </ButtonSecondary>
+        </div>
+      )}
 
-        {existingConditions.length === 0 && (
-          <div className="big-spacer-top">{translate('quality_gates.no_conditions')}</div>
-        )}
-      </div>
-    );
-  }
+      {existingConditions.length === 0 && (
+        <div className="sw-mt-4 sw-body-sm">
+          <LightPrimary as="p">{translate('quality_gates.no_conditions')}</LightPrimary>
+        </div>
+      )}
+    </div>
+  );
 }
 
 export default withMetricsContext(withAvailableFeatures(Conditions));
index 024619c6a36183c0953faed59a360180dfbaa645..0c3652e5113db0af26a9231e8b9e48738fab0bd1 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 { ContentCell, NumericalCell, Table, TableRow, Title } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types';
@@ -35,52 +36,64 @@ interface Props {
   showEdit?: boolean;
 }
 
-export default class ConditionsTable extends React.PureComponent<Props> {
-  render() {
-    const {
-      qualityGate,
-      metrics,
-      canEdit,
-      onRemoveCondition,
-      onSaveCondition,
-      updatedConditionId,
-      scope,
-      conditions,
-      isCaycModal,
-      showEdit,
-    } = this.props;
+function Header() {
+  return (
+    <TableRow>
+      <ContentCell>
+        <Title className="sw-body-sm-highlight sw-m-0 sw-whitespace-nowrap">
+          {translate('quality_gates.conditions.metric')}
+        </Title>
+      </ContentCell>
+      <ContentCell>
+        <Title className="sw-body-sm-highlight sw-m-0 sw-whitespace-nowrap">
+          {translate('quality_gates.conditions.operator')}
+        </Title>
+      </ContentCell>
+      <NumericalCell>
+        <Title className="sw-body-sm-highlight sw-m-0 sw-whitespace-nowrap">
+          {translate('quality_gates.conditions.value')}
+        </Title>
+      </NumericalCell>
+      <ContentCell />
+    </TableRow>
+  );
+}
 
-    return (
-      <table
-        className="data zebra"
-        data-test={`quality-gates__conditions-${scope}`}
-        data-testid={`quality-gates__conditions-${scope}`}
-      >
-        <thead>
-          <tr>
-            <th className="nowrap abs-width-300">{translate('quality_gates.conditions.metric')}</th>
-            <th className="nowrap">{translate('quality_gates.conditions.operator')}</th>
-            <th className="nowrap">{translate('quality_gates.conditions.value')}</th>
-            <th className="thin">&nbsp;</th>
-          </tr>
-        </thead>
-        <tbody>
-          {conditions.map((condition) => (
-            <Condition
-              canEdit={canEdit}
-              condition={condition}
-              key={condition.id}
-              metric={metrics[condition.metric]}
-              onRemoveCondition={onRemoveCondition}
-              onSaveCondition={onSaveCondition}
-              qualityGate={qualityGate}
-              updated={condition.id === updatedConditionId}
-              isCaycModal={isCaycModal}
-              showEdit={showEdit}
-            />
-          ))}
-        </tbody>
-      </table>
-    );
-  }
+export default function ConditionsTable({
+  qualityGate,
+  metrics,
+  canEdit,
+  onRemoveCondition,
+  onSaveCondition,
+  updatedConditionId,
+  scope,
+  conditions,
+  isCaycModal,
+  showEdit,
+}: Props) {
+  return (
+    <Table
+      columnCount={4}
+      columnWidths={['300px', 'auto', 'auto', 'auto']}
+      className="sw-my-2"
+      header={<Header />}
+      data-test={`quality-gates__conditions-${scope}`}
+      data-testid={`quality-gates__conditions-${scope}`}
+    >
+      {conditions.map((condition) => (
+        <Condition
+          canEdit={canEdit}
+          condition={condition}
+          key={condition.id}
+          metric={metrics[condition.metric]}
+          onRemoveCondition={onRemoveCondition}
+          onSaveCondition={onSaveCondition}
+          qualityGate={qualityGate}
+          updated={condition.id === updatedConditionId}
+          isCaycModal={isCaycModal}
+          showEdit={showEdit}
+        />
+      ))}
+    </Table>
+  );
 }
index 3dbcf3e3ddc4ab6027157be4bc4ef6ffffad943a..5fd6dfe27dd431f838f2b20e04f9d84d7db1c8cd 100644 (file)
@@ -42,7 +42,7 @@ it('should open the default quality gates', async () => {
     await screen.findByRole('link', {
       current: 'page',
       name: `${defaultQualityGate.name} default`,
-    }),
+    })
   ).toBeInTheDocument();
 });
 
@@ -52,13 +52,13 @@ it('should list all quality gates', async () => {
   expect(
     await screen.findByRole('link', {
       name: `${handler.getDefaultQualityGate().name} default`,
-    }),
+    })
   ).toBeInTheDocument();
 
   expect(
     screen.getByRole('link', {
       name: `${handler.getBuiltInQualityGate().name} quality_gates.built_in`,
-    }),
+    })
   ).toBeInTheDocument();
 });
 
@@ -226,7 +226,7 @@ it('should be able to add a condition', async () => {
   await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' }));
 
   expect(
-    await overallConditions.findByRole('cell', { name: 'Maintainability Rating' }),
+    await overallConditions.findByRole('cell', { name: 'Maintainability Rating' })
   ).toBeInTheDocument();
   expect(await overallConditions.findByRole('cell', { name: 'B' })).toBeInTheDocument();
 });
@@ -239,7 +239,7 @@ it('should be able to edit a condition', async () => {
   const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
 
   await user.click(
-    newConditions.getByLabelText('quality_gates.condition.edit.Coverage on New Code'),
+    newConditions.getByLabelText('quality_gates.condition.edit.Coverage on New Code')
   );
   const dialog = within(screen.getByRole('dialog'));
   await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
@@ -256,12 +256,12 @@ it('should be able to handle duplicate or deprecated condition', async () => {
 
   await user.click(
     // make it a regexp to ignore badges:
-    await screen.findByRole('link', { name: new RegExp(handler.getCorruptedQualityGateName()) }),
+    await screen.findByRole('link', { name: new RegExp(handler.getCorruptedQualityGateName()) })
   );
 
   expect(await screen.findByText('quality_gates.duplicated_conditions')).toBeInTheDocument();
   expect(
-    await screen.findByRole('cell', { name: 'Complexity / Function deprecated' }),
+    await screen.findByRole('cell', { name: 'Complexity / Function deprecated' })
   ).toBeInTheDocument();
 });
 
@@ -274,7 +274,7 @@ it('should be able to handle delete condition', async () => {
   const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
 
   await user.click(
-    newConditions.getByLabelText('quality_gates.condition.delete.Coverage on New Code'),
+    newConditions.getByLabelText('quality_gates.condition.delete.Coverage on New Code')
   );
 
   const dialog = within(screen.getByRole('dialog'));
@@ -289,10 +289,10 @@ it('should explain condition on branch', async () => {
   renderQualityGateApp({ featureList: [Feature.BranchSupport] });
 
   expect(
-    await screen.findByText('quality_gates.conditions.new_code.description'),
+    await screen.findByText('quality_gates.conditions.new_code.description')
   ).toBeInTheDocument();
   expect(
-    await screen.findByText('quality_gates.conditions.overall_code.description'),
+    await screen.findByText('quality_gates.conditions.overall_code.description')
   ).toBeInTheDocument();
 });
 
@@ -308,29 +308,29 @@ it('should show warning banner when CAYC condition is not properly set and shoul
   expect(screen.getByText('quality_gates.cayc_missing.banner.title')).toBeInTheDocument();
   expect(screen.getByText('quality_gates.cayc_missing.banner.description')).toBeInTheDocument();
   expect(
-    screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' }),
+    screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
   ).toBeInTheDocument();
 
   await user.click(
-    screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' }),
+    screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
   );
   expect(
     screen.getByRole('dialog', {
       name: 'quality_gates.cayc.review_update_modal.header.SonarSource way - CFamily',
-    }),
+    })
   ).toBeInTheDocument();
   expect(
-    screen.getByText('quality_gates.cayc.review_update_modal.description1'),
+    screen.getByText('quality_gates.cayc.review_update_modal.description1')
   ).toBeInTheDocument();
   expect(
-    screen.getByText('quality_gates.cayc.review_update_modal.description2'),
+    screen.getByText('quality_gates.cayc.review_update_modal.description2')
   ).toBeInTheDocument();
   expect(
-    screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' }),
+    screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' })
   ).toBeInTheDocument();
 
   await user.click(
-    screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' }),
+    screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' })
   );
 
   const conditionsWrapper = within(await screen.findByTestId('quality-gates__conditions-new'));
@@ -342,7 +342,7 @@ it('should show warning banner when CAYC condition is not properly set and shoul
   expect(conditionsWrapper.getByText('Duplicated Lines (%)')).toBeInTheDocument();
 
   const overallConditionsWrapper = within(
-    await screen.findByTestId('quality-gates__conditions-overall'),
+    await screen.findByTestId('quality-gates__conditions-overall')
   );
   expect(overallConditionsWrapper.getByText('Complexity / Function')).toBeInTheDocument();
 });
@@ -368,9 +368,7 @@ it('should warn user when quality gate is not CAYC compliant and user has permis
 
   await user.click(nonCompliantQualityGate);
 
-  expect(await screen.findByRole('alert')).toHaveTextContent(
-    /quality_gates.cayc_missing.banner.title/,
-  );
+  expect(await screen.findByText(/quality_gates.cayc_missing.banner.title/)).toBeInTheDocument();
   expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0);
 });
 
@@ -385,13 +383,13 @@ it('should show success banner when quality gate is CAYC compliant', async () =>
 
   expect(screen.getByText('quality_gates.cayc.banner.title')).toBeInTheDocument();
   expect(
-    screen.getByText('quality_gates.cayc.banner.description1', { exact: false }),
+    screen.getByText('quality_gates.cayc.banner.description1', { exact: false })
   ).toBeInTheDocument();
   expect(
-    screen.queryByText('quality_gates.cayc_condition.missing_warning.title'),
+    screen.queryByText('quality_gates.cayc_condition.missing_warning.title')
   ).not.toBeInTheDocument();
   expect(
-    screen.queryByRole('button', { name: 'quality_gates.cayc_condition.review_update' }),
+    screen.queryByRole('button', { name: 'quality_gates.cayc_condition.review_update' })
   ).not.toBeInTheDocument();
 
   const conditionsWrapper = within(await screen.findByTestId('quality-gates__conditions-new'));
@@ -415,24 +413,24 @@ it('should unlock editing option for CAYC conditions', async () => {
   expect(
     screen.queryByRole('button', {
       name: 'quality_gates.condition.edit.Security Rating on New Code',
-    }),
+    })
   ).not.toBeInTheDocument();
   expect(
     screen.queryByRole('button', {
       name: 'quality_gates.condition.delete.Security Rating on New Code',
-    }),
+    })
   ).not.toBeInTheDocument();
 
   await user.click(screen.getByText('quality_gates.cayc.unlock_edit'));
   expect(
     screen.getByRole('button', {
       name: 'quality_gates.condition.edit.Security Rating on New Code',
-    }),
+    })
   ).toBeInTheDocument();
   expect(
     screen.getByRole('button', {
       name: 'quality_gates.condition.delete.Security Rating on New Code',
-    }),
+    })
   ).toBeInTheDocument();
 });
 
@@ -533,7 +531,7 @@ describe('The Permissions section', () => {
     expect(
       await screen.findByRole('link', {
         name: `${handler.getDefaultQualityGate().name} default`,
-      }),
+      })
     ).toBeInTheDocument();
 
     expect(screen.queryByText('quality_gates.permissions')).not.toBeInTheDocument();