]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21012 Remove CaYC compliance check from front-end and rely only on the API...
authorAndrey Luiz <andrey.luiz@sonarsource.com>
Fri, 1 Dec 2023 10:00:02 +0000 (11:00 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 1 Dec 2023 20:02:43 +0000 (20:02 +0000)
21 files changed:
server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
server/sonar-web/src/main/js/app/components/available-features/withAvailableFeatures.tsx
server/sonar-web/src/main/js/app/components/metrics/withMetricsContext.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.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/CopyQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/apps/quality-gates/utils.ts
server/sonar-web/src/main/js/queries/quality-gates.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5b4ce44f7e21539e0fbd416b2b0ddfba65bc2ec5..cfb2574674cbef8dd89fc85b766cb5412dbd07c4 100644 (file)
@@ -129,7 +129,7 @@ export class QualityGatesServiceMock {
             id: 'AXJMbIUHPAOIsUIE3eOi',
             metric: 'new_security_hotspots_reviewed',
             op: 'LT',
-            error: '85',
+            error: '100',
             isCaycCondition: true,
           },
           {
@@ -568,6 +568,13 @@ export class QualityGatesServiceMock {
     this.qualityGateProjectStatus = mockQualityGateProjectStatus(status);
   };
 
+  setCaycStatusForQualityGate = (name: string, caycStatus: CaycStatus) => {
+    const qg = this.list.find((q) => q.name === name);
+    if (qg) {
+      qg.caycStatus = caycStatus;
+    }
+  };
+
   reply<T>(response: T): Promise<T> {
     return Promise.resolve(cloneDeep(response));
   }
index bc6fdc071739b3ab5d72635f0296734b7837285a..5a25ac62230f8f73992890329afc501c9ab8bb50 100644 (file)
@@ -50,3 +50,11 @@ export default function withAvailableFeatures<P>(
     }
   };
 }
+
+export function useAvailableFeatures() {
+  const availableFeatures = React.useContext(AvailableFeaturesContext);
+
+  return {
+    hasFeature: (feature: Feature) => availableFeatures.includes(feature),
+  };
+}
index ac6caa0a9297d31f64eb26e6df3b2ebfdc805afe..7c069e4ac368d5b495548fe29a7fd31145b6f829 100644 (file)
@@ -45,3 +45,7 @@ export default function withMetricsContext<P>(
     }
   };
 }
+
+export function useMetrics() {
+  return React.useContext(MetricsContext);
+}
index 526675bf4b5609b31d79a706a47bf450cd14fdaf..b4292d9d2365c155a7556afe5ffd0fca2ea69f09 100644 (file)
@@ -29,9 +29,9 @@ import {
   themeColor,
 } from 'design-system';
 import * as React from 'react';
+import { useCallback, useEffect } from 'react';
 import { Helmet } from 'react-helmet-async';
-import { NavigateFunction, useNavigate, useParams } from 'react-router-dom';
-import { fetchQualityGates } from '../../../api/quality-gates';
+import { useNavigate, useParams } from 'react-router-dom';
 import Suggestions from '../../../components/embed-docs-modal/Suggestions';
 import '../../../components/search-navigator.css';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -42,140 +42,93 @@ import {
   removeWhitePageClass,
 } from '../../../helpers/pages';
 import { getQualityGateUrl } from '../../../helpers/urls';
+import { useQualityGatesQuery } from '../../../queries/quality-gates';
 import { QualityGate } from '../../../types/types';
 import '../styles.css';
 import Details from './Details';
 import List from './List';
 import ListHeader from './ListHeader';
 
-interface Props {
-  name?: string;
-  navigate: NavigateFunction;
-}
-
-interface State {
-  canCreate: boolean;
-  loading: boolean;
-  qualityGates: QualityGate[];
-}
-
-class App extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { canCreate: false, loading: true, qualityGates: [] };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchQualityGates();
+export default function App() {
+  const { data, isLoading } = useQualityGatesQuery();
+  const { name } = useParams();
+  const navigate = useNavigate();
+  const {
+    qualitygates: qualityGates,
+    actions: { create: canCreate },
+  } = data ?? { qualitygates: [], actions: { create: false } };
+
+  const openDefault = useCallback(
+    (qualityGates?: QualityGate[]) => {
+      if (!qualityGates || qualityGates.length === 0) {
+        return;
+      }
+      const defaultQualityGate = qualityGates.find((gate) => Boolean(gate.isDefault));
+      if (!defaultQualityGate) {
+        return;
+      }
+      navigate(getQualityGateUrl(defaultQualityGate.name), { replace: true });
+    },
+    [navigate],
+  );
+
+  useEffect(() => {
     addWhitePageClass();
     addSideBarClass();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.name !== undefined && this.props.name === undefined) {
-      this.openDefault(this.state.qualityGates);
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-    removeWhitePageClass();
-    removeSideBarClass();
-  }
-
-  fetchQualityGates = () => {
-    return fetchQualityGates().then(
-      ({ actions, qualitygates: qualityGates }) => {
-        if (this.mounted) {
-          this.setState({ canCreate: actions.create, loading: false, qualityGates });
 
-          if (!this.props.name) {
-            this.openDefault(qualityGates);
-          }
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      },
-    );
-  };
+    return () => {
+      removeWhitePageClass();
+      removeSideBarClass();
+    };
+  }, []);
 
-  openDefault(qualityGates: QualityGate[]) {
-    const defaultQualityGate = qualityGates.find((gate) => Boolean(gate.isDefault))!;
-    this.props.navigate(getQualityGateUrl(defaultQualityGate.name), { replace: true });
-  }
-
-  handleSetDefault = (qualityGate: QualityGate) => {
-    this.setState(({ qualityGates }) => {
-      return {
-        qualityGates: qualityGates.map((candidate) => {
-          if (candidate.isDefault || candidate.name === qualityGate.name) {
-            return { ...candidate, isDefault: candidate.name === qualityGate.name };
-          }
-          return candidate;
-        }),
-      };
-    });
-  };
-
-  render() {
-    const { name } = this.props;
-    const { canCreate, qualityGates } = this.state;
-
-    return (
-      <LargeCenteredLayout id="quality-gates-page">
-        <PageContentFontWrapper className="sw-body-sm">
-          <Helmet
-            defer={false}
-            titleTemplate={translateWithParameters(
-              'page_title.template.with_category',
-              translate('quality_gates.page'),
-            )}
-          />
-          <div className="sw-grid sw-gap-x-12 sw-gap-y-6 sw-grid-cols-12 sw-w-full">
-            <Suggestions suggestions="quality_gates" />
-
-            <StyledContentWrapper
-              className="sw-col-span-3 sw-px-4 sw-py-6 sw-border-y-0 sw-rounded-0"
+  useEffect(() => {
+    if (!name) {
+      openDefault(qualityGates);
+    }
+  }, [name, openDefault, qualityGates]);
+
+  return (
+    <LargeCenteredLayout id="quality-gates-page">
+      <PageContentFontWrapper className="sw-body-sm">
+        <Helmet
+          defer={false}
+          titleTemplate={translateWithParameters(
+            'page_title.template.with_category',
+            translate('quality_gates.page'),
+          )}
+        />
+        <div className="sw-grid sw-gap-x-12 sw-gap-y-6 sw-grid-cols-12 sw-w-full">
+          <Suggestions suggestions="quality_gates" />
+
+          <StyledContentWrapper
+            className="sw-col-span-3 sw-px-4 sw-py-6 sw-border-y-0 sw-rounded-0"
+            style={{
+              height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`,
+            }}
+          >
+            <ListHeader canCreate={canCreate} />
+            <Spinner loading={isLoading}>
+              <List qualityGates={qualityGates} currentQualityGate={name} />
+            </Spinner>
+          </StyledContentWrapper>
+
+          {name !== undefined && (
+            <div
+              className="sw-col-span-9 sw-overflow-y-auto"
               style={{
                 height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`,
               }}
             >
-              <ListHeader canCreate={canCreate} refreshQualityGates={this.fetchQualityGates} />
-              <Spinner loading={this.state.loading}>
-                <List qualityGates={qualityGates} currentQualityGate={name} />
-              </Spinner>
-            </StyledContentWrapper>
-
-            {name !== undefined && (
-              <div
-                className="sw-col-span-9 sw-overflow-y-auto"
-                style={{
-                  height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`,
-                }}
-              >
-                <StyledContentWrapper className="sw-my-12">
-                  <Details
-                    qualityGateName={name}
-                    onSetDefault={this.handleSetDefault}
-                    refreshQualityGates={this.fetchQualityGates}
-                  />
-                </StyledContentWrapper>
-              </div>
-            )}
-          </div>
-        </PageContentFontWrapper>
-      </LargeCenteredLayout>
-    );
-  }
-}
-
-export default function AppWrapper() {
-  const params = useParams();
-  const navigate = useNavigate();
-
-  return <App name={params['name']} navigate={navigate} />;
+              <StyledContentWrapper className="sw-my-12">
+                <Details qualityGateName={name} />
+              </StyledContentWrapper>
+            </div>
+          )}
+        </div>
+      </PageContentFontWrapper>
+    </LargeCenteredLayout>
+  );
 }
 
 const StyledContentWrapper = withTheme(styled.div`
index 41bcdfc6558e1c623c59efa819714c8613b04bd4..2d9f5ce3ee6a4b77f769a2c1c7067a73cf1c29a7 100644 (file)
@@ -17,7 +17,6 @@
  * 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 {
   ActionCell,
   ContentCell,
@@ -32,185 +31,149 @@ import {
   TrashIcon,
 } from 'design-system';
 import * as React from 'react';
-import { deleteCondition } from '../../../api/quality-gates';
-import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
+import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
 import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
-import {
-  CaycStatus,
-  Condition as ConditionType,
-  Dict,
-  Metric,
-  QualityGate,
-} from '../../../types/types';
+import { useDeleteConditionMutation } from '../../../queries/quality-gates';
+import { MetricType } from '../../../types/metrics';
+import { CaycStatus, Condition as ConditionType, Metric, QualityGate } from '../../../types/types';
 import { getLocalizedMetricNameNoDiffMetric, isConditionWithFixedValue } from '../utils';
 import ConditionModal from './ConditionModal';
 import ConditionValue from './ConditionValue';
 
+export enum ConditionChange {
+  Added = 'added',
+  Updated = 'updated',
+  Deleted = 'deleted',
+}
+
 interface Props {
   condition: ConditionType;
   canEdit: boolean;
   metric: Metric;
-  onRemoveCondition: (Condition: ConditionType) => void;
-  onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void;
   qualityGate: QualityGate;
-  updated?: boolean;
-  metrics: Dict<Metric>;
   showEdit?: boolean;
   isCaycModal?: boolean;
 }
 
-interface State {
-  deleteFormOpen: boolean;
-  modal: boolean;
-}
-
-export class ConditionComponent extends React.PureComponent<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      deleteFormOpen: false,
-      modal: false,
-    };
-  }
-
-  handleUpdateCondition = (newCondition: ConditionType) => {
-    this.props.onSaveCondition(newCondition, this.props.condition);
-  };
-
-  handleOpenUpdate = () => {
-    this.setState({ modal: true });
-  };
-
-  handleUpdateClose = () => {
-    this.setState({ modal: false });
+export default function ConditionComponent({
+  condition,
+  canEdit,
+  metric,
+  qualityGate,
+  showEdit,
+  isCaycModal,
+}: Readonly<Props>) {
+  const [deleteFormOpen, setDeleteFormOpen] = React.useState(false);
+  const [modal, setModal] = React.useState(false);
+  const { mutateAsync: deleteCondition } = useDeleteConditionMutation(qualityGate.name);
+  const metrics = useMetrics();
+
+  const handleOpenUpdate = () => {
+    setModal(true);
   };
 
-  handleDeleteClick = () => {
-    this.setState({ deleteFormOpen: true });
+  const handleUpdateClose = () => {
+    setModal(false);
   };
 
-  closeDeleteForm = () => {
-    this.setState({ deleteFormOpen: false });
+  const handleDeleteClick = () => {
+    setDeleteFormOpen(true);
   };
 
-  removeCondition = (condition: ConditionType) => {
-    deleteCondition({ id: condition.id }).then(
-      () => this.props.onRemoveCondition(condition),
-      () => {},
-    );
+  const closeDeleteForm = () => {
+    setDeleteFormOpen(false);
   };
 
-  renderOperator() {
-    // TODO can operator be missing?
-    const { op = 'GT' } = this.props.condition;
-    return this.props.metric.type === 'RATING'
+  const renderOperator = () => {
+    const { op = 'GT' } = condition;
+    return metric.type === MetricType.Rating
       ? translate('quality_gates.operator', op, 'rating')
       : translate('quality_gates.operator', op);
-  }
-
-  render() {
-    const {
-      condition,
-      canEdit,
-      metric,
-      qualityGate,
-      updated,
-      metrics,
-      showEdit = true,
-      isCaycModal = false,
-    } = this.props;
-
-    const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant;
-
-    return (
-      <TableRow className={classNames({ highlighted: updated })}>
-        <ContentCell>
-          {getLocalizedMetricNameNoDiffMetric(metric, metrics)}
-          {metric.hidden && <TextError className="sw-ml-1" text={translate('deprecated')} />}
-        </ContentCell>
-
-        <ContentCell className="sw-whitespace-nowrap">{this.renderOperator()}</ContentCell>
+  };
 
-        <NumericalCell className="sw-whitespace-nowrap">
-          <ConditionValue
-            metric={metric}
-            isCaycModal={isCaycModal}
-            condition={condition}
-            isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant}
-          />
-        </NumericalCell>
-        <ActionCell>
-          {!isCaycModal && canEdit && (
-            <>
-              {(!isCaycCompliantAndOverCompliant ||
-                !isConditionWithFixedValue(condition) ||
-                (isCaycCompliantAndOverCompliant && showEdit)) && (
-                <>
-                  <InteractiveIcon
-                    Icon={PencilIcon}
-                    aria-label={translateWithParameters(
-                      'quality_gates.condition.edit',
-                      metric.name,
-                    )}
-                    data-test="quality-gates__condition-update"
-                    onClick={this.handleOpenUpdate}
-                    className="sw-mr-4"
-                    size="small"
+  const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant;
+
+  return (
+    <TableRow>
+      <ContentCell>
+        {getLocalizedMetricNameNoDiffMetric(metric, metrics)}
+        {metric.hidden && <TextError className="sw-ml-1" text={translate('deprecated')} />}
+      </ContentCell>
+
+      <ContentCell className="sw-whitespace-nowrap">{renderOperator()}</ContentCell>
+
+      <NumericalCell className="sw-whitespace-nowrap">
+        <ConditionValue
+          metric={metric}
+          isCaycModal={isCaycModal}
+          condition={condition}
+          isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant}
+        />
+      </NumericalCell>
+      <ActionCell>
+        {!isCaycModal && canEdit && (
+          <>
+            {(!isCaycCompliantAndOverCompliant ||
+              !isConditionWithFixedValue(condition) ||
+              (isCaycCompliantAndOverCompliant && showEdit)) && (
+              <>
+                <InteractiveIcon
+                  Icon={PencilIcon}
+                  aria-label={translateWithParameters('quality_gates.condition.edit', metric.name)}
+                  data-test="quality-gates__condition-update"
+                  onClick={handleOpenUpdate}
+                  className="sw-mr-4"
+                  size="small"
+                />
+                {modal && (
+                  <ConditionModal
+                    condition={condition}
+                    header={translate('quality_gates.update_condition')}
+                    metric={metric}
+                    onClose={handleUpdateClose}
+                    qualityGate={qualityGate}
                   />
-                  {this.state.modal && (
-                    <ConditionModal
-                      condition={condition}
-                      header={translate('quality_gates.update_condition')}
-                      metric={metric}
-                      onAddCondition={this.handleUpdateCondition}
-                      onClose={this.handleUpdateClose}
-                      qualityGate={qualityGate}
-                    />
+                )}
+              </>
+            )}
+            {(!isCaycCompliantAndOverCompliant ||
+              !condition.isCaycCondition ||
+              (isCaycCompliantAndOverCompliant && showEdit)) && (
+              <>
+                <DestructiveIcon
+                  Icon={TrashIcon}
+                  aria-label={translateWithParameters(
+                    'quality_gates.condition.delete',
+                    metric.name,
                   )}
-                </>
-              )}
-              {(!isCaycCompliantAndOverCompliant ||
-                !condition.isCaycCondition ||
-                (isCaycCompliantAndOverCompliant && showEdit)) && (
-                <>
-                  <DestructiveIcon
-                    Icon={TrashIcon}
-                    aria-label={translateWithParameters(
-                      'quality_gates.condition.delete',
-                      metric.name,
+                  onClick={handleDeleteClick}
+                  size="small"
+                />
+                {deleteFormOpen && (
+                  <Modal
+                    headerTitle={translate('quality_gates.delete_condition')}
+                    onClose={closeDeleteForm}
+                    body={translateWithParameters(
+                      'quality_gates.delete_condition.confirm.message',
+                      getLocalizedMetricName(metric),
                     )}
-                    data-test="quality-gates__condition-delete"
-                    onClick={this.handleDeleteClick}
-                    size="small"
+                    primaryButton={
+                      <DangerButtonPrimary
+                        autoFocus
+                        type="submit"
+                        onClick={() => deleteCondition(condition)}
+                      >
+                        {translate('delete')}
+                      </DangerButtonPrimary>
+                    }
+                    secondaryButtonLabel={translate('close')}
                   />
-                  {this.state.deleteFormOpen && (
-                    <Modal
-                      headerTitle={translate('quality_gates.delete_condition')}
-                      onClose={this.closeDeleteForm}
-                      body={translateWithParameters(
-                        'quality_gates.delete_condition.confirm.message',
-                        getLocalizedMetricName(this.props.metric),
-                      )}
-                      primaryButton={
-                        <DangerButtonPrimary
-                          autoFocus
-                          type="submit"
-                          onClick={() => this.removeCondition(condition)}
-                        >
-                          {translate('delete')}
-                        </DangerButtonPrimary>
-                      }
-                      secondaryButtonLabel={translate('close')}
-                    />
-                  )}
-                </>
-              )}
-            </>
-          )}
-        </ActionCell>
-      </TableRow>
-    );
-  }
+                )}
+              </>
+            )}
+          </>
+        )}
+      </ActionCell>
+    </TableRow>
+  );
 }
-
-export default withMetricsContext(ConditionComponent);
index e2bfb2372579c7089a66596923b45eab8f8ff43e..91e110d823ec6075807b771be6322f2aaf305aeb 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 { ButtonPrimary, FlagMessage, FormField, Modal, RadioButton } from 'design-system';
+import { ButtonPrimary, FormField, Modal, RadioButton } from 'design-system';
 import * as React from 'react';
-import { createCondition, updateCondition } from '../../../api/quality-gates';
 import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
 import { isDiffMetric } from '../../../helpers/measures';
+import {
+  useCreateConditionMutation,
+  useUpdateConditionMutation,
+} from '../../../queries/quality-gates';
 import { Condition, Metric, QualityGate } from '../../../types/types';
 import { getPossibleOperators } from '../utils';
 import ConditionOperator from './ConditionOperator';
@@ -33,101 +36,92 @@ interface Props {
   metric?: Metric;
   metrics?: Metric[];
   header: string;
-  onAddCondition: (condition: Condition) => void;
   onClose: () => void;
   qualityGate: QualityGate;
 }
 
-interface State {
-  error: string;
-  errorMessage?: string;
-  metric?: Metric;
-  op?: string;
-  scope: 'new' | 'overall';
-}
-
 const ADD_CONDITION_MODAL_ID = 'add-condition-modal';
 
-export default class ConditionModal extends React.PureComponent<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      error: props.condition ? props.condition.error : '',
-      scope: 'new',
-      metric: props.metric ? props.metric : undefined,
-      op: props.condition ? props.condition.op : undefined,
-    };
-  }
-
-  getSinglePossibleOperator(metric: Metric) {
+export default function ConditionModal({
+  condition,
+  metric,
+  metrics,
+  header,
+  onClose,
+  qualityGate,
+}: Readonly<Props>) {
+  const [errorThreshold, setErrorThreshold] = React.useState(condition ? condition.error : '');
+  const [scope, setScope] = React.useState<'new' | 'overall'>('new');
+  const [selectedMetric, setSelectedMetric] = React.useState<Metric | undefined>(metric);
+  const [selectedOperator, setSelectedOperator] = React.useState<string | undefined>(
+    condition ? condition.op : undefined,
+  );
+  const { mutateAsync: createCondition } = useCreateConditionMutation(qualityGate.name);
+  const { mutateAsync: updateCondition } = useUpdateConditionMutation(qualityGate.name);
+
+  const getSinglePossibleOperator = (metric: Metric) => {
     const operators = getPossibleOperators(metric);
     return Array.isArray(operators) ? undefined : operators;
-  }
+  };
 
-  handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+  const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
     event.preventDefault();
 
-    const { condition, qualityGate } = this.props;
-    const newCondition: Omit<Condition, 'id'> = {
-      metric: this.state.metric!.key,
-      op: this.getSinglePossibleOperator(this.state.metric!) || this.state.op,
-      error: this.state.error,
-    };
-    const submitPromise = condition
-      ? updateCondition({ id: condition.id, ...newCondition })
-      : createCondition({ gateName: qualityGate.name, ...newCondition });
-    return submitPromise.then(this.props.onAddCondition).then(this.props.onClose);
+    if (selectedMetric) {
+      const newCondition: Omit<Condition, 'id'> = {
+        metric: selectedMetric.key,
+        op: getSinglePossibleOperator(selectedMetric) ?? selectedOperator,
+        error: errorThreshold,
+      };
+      const submitPromise = condition
+        ? updateCondition({ id: condition.id, ...newCondition })
+        : createCondition(newCondition);
+      await submitPromise;
+      onClose();
+    }
   };
 
-  handleScopeChange = (scope: 'new' | 'overall') => {
-    this.setState(({ metric }) => {
-      const { metrics } = this.props;
-      let correspondingMetric;
+  const handleScopeChange = (scope: 'new' | 'overall') => {
+    let correspondingMetric;
 
-      if (metric && metrics) {
-        const correspondingMetricKey =
-          scope === 'new' ? `new_${metric.key}` : metric.key.replace(/^new_/, '');
-        correspondingMetric = metrics.find((m) => m.key === correspondingMetricKey);
-      }
+    if (selectedMetric && metrics) {
+      const correspondingMetricKey =
+        scope === 'new' ? `new_${selectedMetric.key}` : selectedMetric.key.replace(/^new_/, '');
+      correspondingMetric = metrics.find((m) => m.key === correspondingMetricKey);
+    }
 
-      return { scope, metric: correspondingMetric };
-    });
+    setScope(scope);
+    setSelectedMetric(correspondingMetric);
   };
 
-  handleMetricChange = (metric: Metric) => {
-    this.setState({ metric, op: undefined, error: '' });
+  const handleMetricChange = (metric: Metric) => {
+    setSelectedMetric(metric);
+    setSelectedOperator(undefined);
+    setErrorThreshold('');
   };
 
-  handleOperatorChange = (op: string) => {
-    this.setState({ op });
+  const handleOperatorChange = (op: string) => {
+    setSelectedOperator(op);
   };
 
-  handleErrorChange = (error: string) => {
-    this.setState({ error });
+  const handleErrorChange = (error: string) => {
+    setErrorThreshold(error);
   };
 
-  renderBody = () => {
-    const { metrics } = this.props;
-    const { op, error, scope, metric, errorMessage } = this.state;
-
+  const renderBody = () => {
     return (
-      <form id={ADD_CONDITION_MODAL_ID} onSubmit={this.handleFormSubmit}>
-        {errorMessage && (
-          <FlagMessage className="sw-mb-2" variant="error">
-            {errorMessage}
-          </FlagMessage>
-        )}
-        {this.props.metric === undefined && (
+      <form id={ADD_CONDITION_MODAL_ID} onSubmit={handleFormSubmit}>
+        {metric === undefined && (
           <FormField label={translate('quality_gates.conditions.where')}>
             <div className="sw-flex sw-gap-4">
-              <RadioButton checked={scope === 'new'} onCheck={this.handleScopeChange} value="new">
+              <RadioButton checked={scope === 'new'} onCheck={handleScopeChange} value="new">
                 <span data-test="quality-gates__condition-scope-new">
                   {translate('quality_gates.conditions.new_code')}
                 </span>
               </RadioButton>
               <RadioButton
                 checked={scope === 'overall'}
-                onCheck={this.handleScopeChange}
+                onCheck={handleScopeChange}
                 value="overall"
               >
                 <span data-test="quality-gates__condition-scope-overall">
@@ -139,22 +133,22 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
         )}
 
         <FormField
-          description={this.props.metric && getLocalizedMetricName(this.props.metric)}
+          description={metric && getLocalizedMetricName(metric)}
           htmlFor="condition-metric"
           label={translate('quality_gates.conditions.fails_when')}
         >
           {metrics && (
             <MetricSelect
-              metric={metric}
+              metric={selectedMetric}
               metricsArray={metrics.filter((m) =>
                 scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key),
               )}
-              onMetricChange={this.handleMetricChange}
+              onMetricChange={handleMetricChange}
             />
           )}
         </FormField>
 
-        {metric && (
+        {selectedMetric && (
           <div className="sw-flex sw-gap-2">
             <FormField
               className="sw-mb-0"
@@ -162,9 +156,9 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
               label={translate('quality_gates.conditions.operator')}
             >
               <ConditionOperator
-                metric={metric}
-                onOperatorChange={this.handleOperatorChange}
-                op={op}
+                metric={selectedMetric}
+                onOperatorChange={handleOperatorChange}
+                op={selectedOperator}
               />
             </FormField>
             <FormField
@@ -172,10 +166,10 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
               label={translate('quality_gates.conditions.value')}
             >
               <ThresholdInput
-                metric={metric}
+                metric={selectedMetric}
                 name="error"
-                onChange={this.handleErrorChange}
-                value={error}
+                onChange={handleErrorChange}
+                value={errorThreshold}
               />
             </FormField>
           </div>
@@ -184,29 +178,25 @@ export default class ConditionModal extends React.PureComponent<Props, State> {
     );
   };
 
-  render() {
-    const { header } = this.props;
-    const { metric } = this.state;
-    return (
-      <Modal
-        isScrollable={false}
-        isOverflowVisible
-        headerTitle={header}
-        onClose={this.props.onClose}
-        body={this.renderBody()}
-        primaryButton={
-          <ButtonPrimary
-            autoFocus
-            disabled={metric === undefined}
-            id="add-condition-button"
-            form={ADD_CONDITION_MODAL_ID}
-            type="submit"
-          >
-            {header}
-          </ButtonPrimary>
-        }
-        secondaryButtonLabel={translate('close')}
-      />
-    );
-  }
+  return (
+    <Modal
+      isScrollable={false}
+      isOverflowVisible
+      headerTitle={header}
+      onClose={onClose}
+      body={renderBody()}
+      primaryButton={
+        <ButtonPrimary
+          autoFocus
+          disabled={selectedMetric === undefined}
+          id="add-condition-button"
+          form={ADD_CONDITION_MODAL_ID}
+          type="submit"
+        >
+          {header}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('close')}
+    />
+  );
 }
index 63b9d78ca1487964051837c2fc116ce84cecb8bb..879d26deb320fb371846fa360eaf09ea2f40e791 100644 (file)
@@ -21,39 +21,27 @@ import { ButtonPrimary, Link, Modal, SubHeading, Title } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { createCondition, updateCondition } from '../../../api/quality-gates';
 import { useDocUrl } from '../../../helpers/docs';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { useFixQualityGateMutation } from '../../../queries/quality-gates';
 import { Condition, Dict, Metric, QualityGate } from '../../../types/types';
-import { getCorrectCaycCondition, getWeakMissingAndNonCaycConditions } from '../utils';
+import { getWeakMissingAndNonCaycConditions } from '../utils';
 import ConditionsTable from './ConditionsTable';
 
 interface Props {
   canEdit: boolean;
   metrics: Dict<Metric>;
-  updatedConditionId?: string;
   conditions: Condition[];
   scope: 'new' | 'overall' | 'new-cayc';
   onClose: () => void;
-  onAddCondition: (condition: Condition) => void;
-  onRemoveCondition: (condition: Condition) => void;
-  onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void;
   lockEditing: () => void;
   qualityGate: QualityGate;
   isOptimizing?: boolean;
 }
 
 export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) {
-  const {
-    conditions,
-    qualityGate,
-    metrics,
-    onSaveCondition,
-    onAddCondition,
-    lockEditing,
-    onClose,
-    isOptimizing,
-  } = props;
+  const { conditions, qualityGate, metrics, lockEditing, onClose, isOptimizing } = props;
+  const { mutateAsync: fixQualityGate } = useFixQualityGateMutation(qualityGate.name);
 
   const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions);
   const sortedWeakConditions = sortBy(
@@ -68,43 +56,10 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>)
 
   const getDocUrl = useDocUrl();
 
-  const updateCaycQualityGate = React.useCallback(() => {
-    const promiseArr: Promise<Condition | undefined | void>[] = [];
-    const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions);
-
-    weakConditions.forEach((condition) => {
-      promiseArr.push(
-        updateCondition({
-          ...getCorrectCaycCondition(condition),
-          id: condition.id,
-        })
-          .then((resultCondition) => {
-            const currentCondition = conditions.find((con) => con.metric === condition.metric);
-            if (currentCondition) {
-              onSaveCondition({ ...resultCondition, isCaycCondition: true }, currentCondition);
-            }
-          })
-          .catch(() => undefined),
-      );
-    });
-
-    missingConditions.forEach((condition) => {
-      promiseArr.push(
-        createCondition({
-          ...getCorrectCaycCondition(condition),
-          gateName: qualityGate.name,
-        })
-          .then((resultCondition) => {
-            onAddCondition({ ...resultCondition, isCaycCondition: true });
-          })
-          .catch(() => undefined),
-      );
-    });
-
-    return Promise.all(promiseArr).then(() => {
-      lockEditing();
-    });
-  }, [conditions, qualityGate, lockEditing, onAddCondition, onSaveCondition]);
+  const updateCaycQualityGate = React.useCallback(async () => {
+    await fixQualityGate({ weakConditions, missingConditions });
+    lockEditing();
+  }, [fixQualityGate, weakConditions, missingConditions, lockEditing]);
 
   const body = (
     <div className="sw-mb-10">
index c3b9ec5c0521d937f16d185064839110b29f3e44..3278266849795bd0face14f392958b69a8856c72 100644 (file)
@@ -31,23 +31,15 @@ import {
 import { differenceWith, map, 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 { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
+import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
 import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
 import ModalButton, { ModalProps } from '../../../components/controls/ModalButton';
 import { useDocUrl } from '../../../helpers/docs';
 import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
 import { Feature } from '../../../types/features';
 import { MetricKey } from '../../../types/metrics';
-import {
-  CaycStatus,
-  Condition as ConditionType,
-  Dict,
-  Metric,
-  QualityGate,
-} from '../../../types/types';
+import { CaycStatus, Condition as ConditionType, QualityGate } from '../../../types/types';
 import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils';
 import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide';
 import CaycCompliantBanner from './CaycCompliantBanner';
@@ -57,13 +49,8 @@ import ConditionModal from './ConditionModal';
 import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal';
 import ConditionsTable from './ConditionsTable';
 
-interface Props extends WithAvailableFeaturesProps {
-  metrics: Dict<Metric>;
-  onAddCondition: (condition: ConditionType) => void;
-  onRemoveCondition: (Condition: ConditionType) => void;
-  onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void;
+interface Props {
   qualityGate: QualityGate;
-  updatedConditionId?: string;
 }
 
 const FORBIDDEN_METRIC_TYPES = ['DATA', 'DISTRIB', 'STRING', 'BOOL'];
@@ -74,19 +61,12 @@ const FORBIDDEN_METRICS: string[] = [
   MetricKey.new_security_hotspots,
 ];
 
-function Conditions({
-  qualityGate,
-  metrics,
-  onRemoveCondition,
-  onSaveCondition,
-  onAddCondition,
-  hasFeature,
-  updatedConditionId,
-}: Readonly<Props>) {
+export default function Conditions({ qualityGate }: Readonly<Props>) {
   const [editing, setEditing] = React.useState<boolean>(
     qualityGate.caycStatus === CaycStatus.NonCompliant,
   );
   const { name } = qualityGate;
+  const metrics = useMetrics();
   const canEdit = Boolean(qualityGate.actions?.manageConditions);
   const { conditions = [] } = qualityGate;
   const existingConditions = conditions.filter((condition) => metrics[condition.metric]);
@@ -101,6 +81,7 @@ function Conditions({
       duplicates.push(condition);
     }
   });
+  const { hasFeature } = useAvailableFeatures();
 
   const uniqDuplicates = uniqBy(duplicates, (d) => d.metric).map((condition) => ({
     ...condition,
@@ -130,13 +111,12 @@ function Conditions({
         <ConditionModal
           header={translate('quality_gates.add_condition')}
           metrics={availableMetrics}
-          onAddCondition={onAddCondition}
           onClose={onClose}
           qualityGate={qualityGate}
         />
       );
     },
-    [metrics, qualityGate, onAddCondition],
+    [metrics, qualityGate],
   );
 
   const getDocUrl = useDocUrl();
@@ -153,11 +133,7 @@ function Conditions({
           qualityGate={qualityGate}
           metrics={metrics}
           canEdit={canEdit}
-          onRemoveCondition={onRemoveCondition}
-          onSaveCondition={onSaveCondition}
-          onAddCondition={onAddCondition}
           lockEditing={() => setEditing(false)}
-          updatedConditionId={updatedConditionId}
           conditions={conditions}
           scope="new-cayc"
           onClose={onClose}
@@ -165,15 +141,7 @@ function Conditions({
         />
       );
     },
-    [
-      qualityGate,
-      metrics,
-      updatedConditionId,
-      onAddCondition,
-      onRemoveCondition,
-      onSaveCondition,
-      isOptimizing,
-    ],
+    [qualityGate, metrics, isOptimizing],
   );
 
   return (
@@ -267,9 +235,6 @@ function Conditions({
               qualityGate={qualityGate}
               metrics={metrics}
               canEdit={canEdit}
-              onRemoveCondition={onRemoveCondition}
-              onSaveCondition={onSaveCondition}
-              updatedConditionId={updatedConditionId}
               conditions={newCodeConditions}
               showEdit={editing}
               scope="new"
@@ -293,9 +258,6 @@ function Conditions({
               qualityGate={qualityGate}
               metrics={metrics}
               canEdit={canEdit}
-              onRemoveCondition={onRemoveCondition}
-              onSaveCondition={onSaveCondition}
-              updatedConditionId={updatedConditionId}
               conditions={overallCodeConditions}
               scope="overall"
             />
@@ -338,5 +300,3 @@ function Conditions({
     </div>
   );
 }
-
-export default withMetricsContext(withAvailableFeatures(Conditions));
index b062ed10504bdd979159f76b4ccec4965da69e96..63e7aaee9ae6a245872fdcd7529f5822c28a5ac1 100644 (file)
@@ -26,14 +26,11 @@ import Condition from './Condition';
 interface Props {
   canEdit: boolean;
   metrics: Dict<Metric>;
-  onRemoveCondition: (Condition: ConditionType) => void;
-  onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void;
   qualityGate: QualityGate;
-  updatedConditionId?: string;
   conditions: ConditionType[];
   scope: 'new' | 'overall' | 'new-cayc';
-  isCaycModal?: boolean;
   showEdit?: boolean;
+  isCaycModal?: boolean;
 }
 
 function Header() {
@@ -63,9 +60,6 @@ export default function ConditionsTable({
   qualityGate,
   metrics,
   canEdit,
-  onRemoveCondition,
-  onSaveCondition,
-  updatedConditionId,
   scope,
   conditions,
   isCaycModal,
@@ -86,10 +80,7 @@ export default function ConditionsTable({
           condition={condition}
           key={condition.id}
           metric={metrics[condition.metric]}
-          onRemoveCondition={onRemoveCondition}
-          onSaveCondition={onSaveCondition}
           qualityGate={qualityGate}
-          updated={condition.id === updatedConditionId}
           isCaycModal={isCaycModal}
           showEdit={showEdit}
         />
index 13193152b680e3f84bc9428b50b8266432feaa94..d7317466b4daa2c3b3b5a2dbf7f632bdd0fc0d2e 100644 (file)
  */
 import { ButtonPrimary, FormField, InputField, Modal } from 'design-system';
 import * as React from 'react';
-import { copyQualityGate } from '../../../api/quality-gates';
-import { Router, withRouter } from '../../../components/hoc/withRouter';
+import { useRouter } from '../../../components/hoc/withRouter';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
+import { useCopyQualityGateMutation } from '../../../queries/quality-gates';
 import { QualityGate } from '../../../types/types';
 
 interface Props {
   onClose: () => void;
-  onCopy: () => Promise<void>;
   qualityGate: QualityGate;
-  router: Router;
-}
-
-interface State {
-  name: string;
 }
 
 const FORM_ID = 'rename-quality-gate';
 
-export class CopyQualityGateForm extends React.PureComponent<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = { name: props.qualityGate.name };
-  }
+export default function CopyQualityGateForm({ qualityGate, onClose }: Readonly<Props>) {
+  const [name, setName] = React.useState(qualityGate.name);
+  const { mutateAsync: copyQualityGate } = useCopyQualityGateMutation(qualityGate.name);
+  const router = useRouter();
 
-  handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.setState({ name: event.currentTarget.value });
+  const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setName(event.currentTarget.value);
   };
 
-  handleCopy = (event: React.FormEvent) => {
+  const handleCopy = async (event: React.FormEvent) => {
     event.preventDefault();
 
-    const { qualityGate } = this.props;
-    const { name } = this.state;
-
-    return copyQualityGate({ sourceName: qualityGate.name, name }).then((newQualityGate) => {
-      this.props.onCopy();
-      this.props.router.push(getQualityGateUrl(newQualityGate.name));
-    });
+    const newQualityGate = await copyQualityGate(name);
+    router.push(getQualityGateUrl(newQualityGate.name));
   };
 
-  render() {
-    const { qualityGate } = this.props;
-    const { name } = this.state;
-    const buttonDisabled = !name || (qualityGate && qualityGate.name === name);
+  const buttonDisabled = !name || (qualityGate && qualityGate.name === name);
 
-    return (
-      <Modal
-        headerTitle={translate('quality_gates.copy')}
-        onClose={this.props.onClose}
-        body={
-          <form id={FORM_ID} onSubmit={this.handleCopy}>
-            <MandatoryFieldsExplanation />
-            <FormField
-              label={translate('name')}
-              htmlFor="quality-gate-form-name"
-              required
-              className="sw-my-2"
-            >
-              <InputField
-                autoFocus
-                id="quality-gate-form-name"
-                maxLength={100}
-                onChange={this.handleNameChange}
-                size="auto"
-                type="text"
-                value={name}
-              />
-            </FormField>
-          </form>
-        }
-        primaryButton={
-          <ButtonPrimary autoFocus type="submit" disabled={buttonDisabled} form={FORM_ID}>
-            {translate('copy')}
-          </ButtonPrimary>
-        }
-        secondaryButtonLabel={translate('cancel')}
-      />
-    );
-  }
+  return (
+    <Modal
+      headerTitle={translate('quality_gates.copy')}
+      onClose={onClose}
+      body={
+        <form id={FORM_ID} onSubmit={handleCopy}>
+          <MandatoryFieldsExplanation />
+          <FormField
+            label={translate('name')}
+            htmlFor="quality-gate-form-name"
+            required
+            className="sw-my-2"
+          >
+            <InputField
+              autoFocus
+              id="quality-gate-form-name"
+              maxLength={100}
+              onChange={handleNameChange}
+              size="auto"
+              type="text"
+              value={name}
+            />
+          </FormField>
+        </form>
+      }
+      primaryButton={
+        <ButtonPrimary autoFocus type="submit" disabled={buttonDisabled} form={FORM_ID}>
+          {translate('copy')}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
+  );
 }
-
-export default withRouter(CopyQualityGateForm);
index 92b6c630adeeaf633d6c9526ad8f0a78e73511be..176f5689fee51148f24b7aa8aff08d6da514ffee 100644 (file)
  */
 import { ButtonSecondary, FormField, InputField, Modal } from 'design-system';
 import * as React from 'react';
-import { createQualityGate } from '../../../api/quality-gates';
-import { Router, withRouter } from '../../../components/hoc/withRouter';
+import { useRouter } from '../../../components/hoc/withRouter';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
+import { useCreateQualityGateMutation } from '../../../queries/quality-gates';
 
 interface Props {
   onClose: () => void;
-  onCreate: () => Promise<void>;
-  router: Router;
 }
 
-interface State {
-  name: string;
-}
-
-export class CreateQualityGateForm extends React.PureComponent<Props, State> {
-  state: State = { name: '' };
-
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
-    this.setState({ name: event.currentTarget.value });
-  };
+export default function CreateQualityGateForm({ onClose }: Readonly<Props>) {
+  const [name, setName] = React.useState('');
+  const { mutateAsync: createQualityGate } = useCreateQualityGateMutation();
+  const router = useRouter();
 
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
-    this.handleCreate();
+  const handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+    setName(event.currentTarget.value);
   };
 
-  handleCreate = async () => {
-    const { name } = this.state;
-
+  const handleCreate = async () => {
     if (name !== undefined) {
-      const qualityGate = await createQualityGate({ name });
-      await this.props.onCreate();
-      this.props.onClose();
-      this.props.router.push(getQualityGateUrl(qualityGate.name));
+      const qualityGate = await createQualityGate(name);
+      onClose();
+      router.push(getQualityGateUrl(qualityGate.name));
     }
   };
 
-  render() {
-    const { name } = this.state;
+  const handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    handleCreate();
+  };
 
-    const body = (
-      <form onSubmit={this.handleFormSubmit}>
-        <MandatoryFieldsExplanation className="modal-field" />
-        <FormField
-          htmlFor="quality-gate-form-name"
-          label={translate('name')}
-          required
-          requiredAriaLabel={translate('field_required')}
-        >
-          <InputField
-            className="sw-mb-1"
-            autoComplete="off"
-            id="quality-gate-form-name"
-            maxLength={256}
-            name="key"
-            onChange={this.handleNameChange}
-            type="text"
-            size="full"
-            value={name}
-          />
-        </FormField>
-      </form>
-    );
+  const body = (
+    <form onSubmit={handleFormSubmit}>
+      <MandatoryFieldsExplanation className="modal-field" />
+      <FormField
+        htmlFor="quality-gate-form-name"
+        label={translate('name')}
+        required
+        requiredAriaLabel={translate('field_required')}
+      >
+        <InputField
+          className="sw-mb-1"
+          autoComplete="off"
+          id="quality-gate-form-name"
+          maxLength={256}
+          name="key"
+          onChange={handleNameChange}
+          type="text"
+          size="full"
+          value={name}
+        />
+      </FormField>
+    </form>
+  );
 
-    return (
-      <Modal
-        onClose={this.props.onClose}
-        headerTitle={translate('quality_gates.create')}
-        isScrollable
-        body={body}
-        primaryButton={
-          <ButtonSecondary
-            disabled={name === null || name === ''}
-            form="create-application-form"
-            type="submit"
-            onClick={this.handleCreate}
-          >
-            {translate('quality_gate.create')}
-          </ButtonSecondary>
-        }
-        secondaryButtonLabel={translate('cancel')}
-      />
-    );
-  }
+  return (
+    <Modal
+      onClose={onClose}
+      headerTitle={translate('quality_gates.create')}
+      isScrollable
+      body={body}
+      primaryButton={
+        <ButtonSecondary
+          disabled={name === null || name === ''}
+          form="create-application-form"
+          type="submit"
+          onClick={handleCreate}
+        >
+          {translate('quality_gate.create')}
+        </ButtonSecondary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
+  );
 }
-
-export default withRouter(CreateQualityGateForm);
index a022e1115540059bab230742c285d178affa3b42..13ed58276b17ac81bb08c739c686b51a35199622 100644 (file)
  */
 import { DangerButtonPrimary, Modal } from 'design-system';
 import * as React from 'react';
-import { deleteQualityGate } from '../../../api/quality-gates';
-import { Router, withRouter } from '../../../components/hoc/withRouter';
+import { useRouter } from '../../../components/hoc/withRouter';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getQualityGatesUrl } from '../../../helpers/urls';
+import { useDeleteQualityGateMutation } from '../../../queries/quality-gates';
 import { QualityGate } from '../../../types/types';
 
 interface Props {
-  readonly onClose: () => void;
-  onDelete: () => Promise<void>;
+  onClose: () => void;
   qualityGate: QualityGate;
-  router: Router;
 }
 
-export class DeleteQualityGateForm extends React.PureComponent<Props> {
-  onDelete = () => {
-    const { qualityGate } = this.props;
-    return deleteQualityGate({ name: qualityGate.name })
-      .then(this.props.onDelete)
-      .then(() => {
-        this.props.router.push(getQualityGatesUrl());
-      });
-  };
+export default function DeleteQualityGateForm({ qualityGate, onClose }: Readonly<Props>) {
+  const { mutateAsync: deleteQualityGate } = useDeleteQualityGateMutation(qualityGate.name);
+  const router = useRouter();
 
-  render() {
-    const { qualityGate } = this.props;
+  const onDelete = async () => {
+    await deleteQualityGate();
+    router.push(getQualityGatesUrl());
+  };
 
-    return (
-      <Modal
-        headerTitle={translate('quality_gates.delete')}
-        onClose={this.props.onClose}
-        body={translateWithParameters('quality_gates.delete.confirm.message', qualityGate.name)}
-        primaryButton={
-          <DangerButtonPrimary autoFocus type="submit" onClick={this.onDelete}>
-            {translate('delete')}
-          </DangerButtonPrimary>
-        }
-        secondaryButtonLabel={translate('cancel')}
-      />
-    );
-  }
+  return (
+    <Modal
+      headerTitle={translate('quality_gates.delete')}
+      onClose={onClose}
+      body={translateWithParameters('quality_gates.delete.confirm.message', qualityGate.name)}
+      primaryButton={
+        <DangerButtonPrimary autoFocus type="submit" onClick={onDelete}>
+          {translate('delete')}
+        </DangerButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
+  );
 }
-
-export default withRouter(DeleteQualityGateForm);
index e54437d6ad008aef56d6ec5dbd4e61d2bf386aa6..e34dc06d2fcb62a33cdfa2762e17d2a732ded7ef 100644 (file)
  */
 
 import { Spinner } from 'design-system';
-import { clone } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
-import { fetchQualityGate } from '../../../api/quality-gates';
-import { addGlobalSuccessMessage } from '../../../helpers/globalMessages';
-import { translate } from '../../../helpers/l10n';
-import { Condition, QualityGate } from '../../../types/types';
-import { addCondition, deleteCondition, replaceCondition } from '../utils';
+import { useQualityGateQuery } from '../../../queries/quality-gates';
 import DetailsContent from './DetailsContent';
 import DetailsHeader from './DetailsHeader';
 
 interface Props {
   qualityGateName: string;
-  onSetDefault: (qualityGate: QualityGate) => void;
-  refreshQualityGates: () => Promise<void>;
 }
 
-interface State {
-  loading: boolean;
-  qualityGate?: QualityGate;
-  updatedConditionId?: string;
-}
-
-export default class Details extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: true };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchDetails();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.qualityGateName !== this.props.qualityGateName) {
-      this.fetchDetails();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchDetails = () => {
-    const { qualityGateName } = this.props;
-
-    this.setState({ loading: true });
-    return fetchQualityGate({ name: qualityGateName }).then(
-      (qualityGate) => {
-        if (this.mounted) {
-          this.setState({ loading: false, qualityGate, updatedConditionId: undefined });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      },
-    );
-  };
-
-  handleAddCondition = (condition: Condition) => {
-    this.setState(({ qualityGate }) => {
-      if (!qualityGate) {
-        return null;
-      }
-      addGlobalSuccessMessage(translate('quality_gates.condition_added'));
-
-      const updatedQualityGate = addCondition(clone(qualityGate), condition);
-      if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) {
-        this.props.refreshQualityGates();
-      }
-
-      return {
-        qualityGate: updatedQualityGate,
-        updatedConditionId: condition.id,
-      };
-    });
-  };
-
-  handleSaveCondition = (newCondition: Condition, oldCondition: Condition) => {
-    this.setState(({ qualityGate }) => {
-      if (!qualityGate) {
-        return null;
-      }
-      addGlobalSuccessMessage(translate('quality_gates.condition_updated'));
-      const updatedQualityGate = replaceCondition(clone(qualityGate), newCondition, oldCondition);
-      if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) {
-        this.props.refreshQualityGates();
-      }
-      return {
-        qualityGate: updatedQualityGate,
-        updatedConditionId: newCondition.id,
-      };
-    });
-  };
-
-  handleRemoveCondition = (condition: Condition) => {
-    this.setState(({ qualityGate }) => {
-      if (!qualityGate) {
-        return null;
-      }
-      addGlobalSuccessMessage(translate('quality_gates.condition_deleted'));
-      const updatedQualityGate = deleteCondition(clone(qualityGate), condition);
-      if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) {
-        this.props.refreshQualityGates();
-      }
-      return {
-        qualityGate: updatedQualityGate,
-        updatedConditionId: undefined,
-      };
-    });
-  };
-
-  handleSetDefault = () => {
-    this.setState(({ qualityGate }) => {
-      if (!qualityGate) {
-        return null;
-      }
-      this.props.onSetDefault(qualityGate);
-      const newQualityGate: QualityGate = {
-        ...qualityGate,
-        actions: { ...qualityGate.actions, delete: false, setAsDefault: false },
-      };
-      return { qualityGate: newQualityGate };
-    });
-  };
-
-  render() {
-    const { refreshQualityGates } = this.props;
-    const { loading, qualityGate, updatedConditionId } = this.state;
-
-    return (
-      <main className="layout-page-main">
-        <Spinner loading={loading}>
-          {qualityGate && (
-            <>
-              <Helmet defer={false} title={qualityGate.name} />
-              <DetailsHeader
-                onSetDefault={this.handleSetDefault}
-                qualityGate={qualityGate}
-                refreshItem={this.fetchDetails}
-                refreshList={refreshQualityGates}
-              />
-              <DetailsContent
-                onAddCondition={this.handleAddCondition}
-                onRemoveCondition={this.handleRemoveCondition}
-                onSaveCondition={this.handleSaveCondition}
-                qualityGate={qualityGate}
-                updatedConditionId={updatedConditionId}
-              />
-            </>
-          )}
-        </Spinner>
-      </main>
-    );
-  }
+export default function Details({ qualityGateName }: Readonly<Props>) {
+  const { data: qualityGate, isLoading } = useQualityGateQuery(qualityGateName);
+
+  return (
+    <main className="layout-page-main">
+      <Spinner loading={isLoading}>
+        {qualityGate && (
+          <>
+            <Helmet defer={false} title={qualityGate.name} />
+            <DetailsHeader qualityGate={qualityGate} />
+            <DetailsContent qualityGate={qualityGate} />
+          </>
+        )}
+      </Spinner>
+    </main>
+  );
 }
index b65ddc0e4d477405f8edf3db191e6e6806a1501d..ec87de8cf81b7878d6e0410c95c3766de2e61f94 100644 (file)
@@ -21,21 +21,17 @@ import { FlagMessage, HelperHintIcon, SubTitle } from 'design-system';
 import * as React from 'react';
 import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
 import { translate } from '../../../helpers/l10n';
-import { Condition, QualityGate } from '../../../types/types';
+import { QualityGate } from '../../../types/types';
 import Conditions from './Conditions';
 import Projects from './Projects';
 import QualityGatePermissions from './QualityGatePermissions';
 
 export interface DetailsContentProps {
-  onAddCondition: (condition: Condition) => void;
-  onRemoveCondition: (Condition: Condition) => void;
-  onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void;
   qualityGate: QualityGate;
-  updatedConditionId?: string;
 }
 
 export function DetailsContent(props: DetailsContentProps) {
-  const { qualityGate, updatedConditionId } = props;
+  const { qualityGate } = props;
   const actions = qualityGate.actions || {};
 
   return (
@@ -47,13 +43,7 @@ export function DetailsContent(props: DetailsContentProps) {
           </FlagMessage>
         )}
 
-      <Conditions
-        onAddCondition={props.onAddCondition}
-        onRemoveCondition={props.onRemoveCondition}
-        onSaveCondition={props.onSaveCondition}
-        qualityGate={qualityGate}
-        updatedConditionId={updatedConditionId}
-      />
+      <Conditions qualityGate={qualityGate} />
 
       <div className="sw-mt-10">
         <div className="sw-flex sw-flex-col">
index 8a80d6826c7ed627e374c500356692e0c537c9f2..d07397a9d7072be10a99ef22e59c7c2c5ea82843 100644 (file)
@@ -33,10 +33,10 @@ import {
 import { countBy } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { setQualityGateAsDefault } from '../../../api/quality-gates';
 import DocumentationLink from '../../../components/common/DocumentationLink';
 import Tooltip from '../../../components/controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
+import { useSetQualityGateAsDefaultMutation } from '../../../queries/quality-gates';
 import { CaycStatus, QualityGate } from '../../../types/types';
 import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
 import CaycBadgeTooltip from './CaycBadgeTooltip';
@@ -45,20 +45,12 @@ import DeleteQualityGateForm from './DeleteQualityGateForm';
 import RenameQualityGateForm from './RenameQualityGateForm';
 
 interface Props {
-  onSetDefault: () => void;
   qualityGate: QualityGate;
-  refreshItem: () => Promise<void>;
-  refreshList: () => Promise<void>;
 }
 
 const TOOLTIP_MOUSE_LEAVE_DELAY = 0.3;
 
-export default function DetailsHeader({
-  refreshItem,
-  refreshList,
-  onSetDefault,
-  qualityGate,
-}: Readonly<Props>) {
+export default function DetailsHeader({ qualityGate }: Readonly<Props>) {
   const [isRenameFormOpen, setIsRenameFormOpen] = React.useState(false);
   const [isCopyFormOpen, setIsCopyFormOpen] = React.useState(false);
   const [isRemoveFormOpen, setIsRemoveFormOpen] = React.useState(false);
@@ -70,22 +62,13 @@ export default function DetailsHeader({
     actions.setAsDefault,
   ])['true'];
   const canEdit = Boolean(actions?.manageConditions);
-
-  const handleActionRefresh = () => {
-    return Promise.all([refreshItem(), refreshList()]).then(
-      () => {},
-      () => {},
-    );
-  };
+  const { mutateAsync: setQualityGateAsDefault } = useSetQualityGateAsDefaultMutation(
+    qualityGate.name,
+  );
 
   const handleSetAsDefaultClick = () => {
     if (!qualityGate.isDefault) {
-      // Optimistic update
-      onSetDefault();
-      setQualityGateAsDefault({ name: qualityGate.name }).then(
-        handleActionRefresh,
-        handleActionRefresh,
-      );
+      setQualityGateAsDefault({ name: qualityGate.name });
     }
   };
 
@@ -237,23 +220,17 @@ export default function DetailsHeader({
       {isRenameFormOpen && (
         <RenameQualityGateForm
           onClose={() => setIsRenameFormOpen(false)}
-          onRename={handleActionRefresh}
           qualityGate={qualityGate}
         />
       )}
 
       {isCopyFormOpen && (
-        <CopyQualityGateForm
-          onClose={() => setIsCopyFormOpen(false)}
-          onCopy={handleActionRefresh}
-          qualityGate={qualityGate}
-        />
+        <CopyQualityGateForm onClose={() => setIsCopyFormOpen(false)} qualityGate={qualityGate} />
       )}
 
       {isRemoveFormOpen && (
         <DeleteQualityGateForm
           onClose={() => setIsRemoveFormOpen(false)}
-          onDelete={refreshList}
           qualityGate={qualityGate}
         />
       )}
index 609eb5a2c214da15b066cff25cda02ea917383a4..c0548308dd17dee7a20fdafad45021680a7b25ae 100644 (file)
@@ -26,17 +26,12 @@ import CreateQualityGateForm from './CreateQualityGateForm';
 
 interface Props {
   canCreate: boolean;
-  refreshQualityGates: () => Promise<void>;
 }
 
-function CreateQualityGateModal({
-  refreshQualityGates,
-}: Readonly<Pick<Props, 'refreshQualityGates'>>) {
+function CreateQualityGateModal() {
   const renderModal = React.useCallback(
-    ({ onClose }: ModalProps) => (
-      <CreateQualityGateForm onClose={onClose} onCreate={refreshQualityGates} />
-    ),
-    [refreshQualityGates],
+    ({ onClose }: ModalProps) => <CreateQualityGateForm onClose={onClose} />,
+    [],
   );
 
   return (
@@ -52,7 +47,7 @@ function CreateQualityGateModal({
   );
 }
 
-export default function ListHeader({ canCreate, refreshQualityGates }: Readonly<Props>) {
+export default function ListHeader({ canCreate }: Readonly<Props>) {
   return (
     <div className="sw-flex sw-justify-between sw-pb-4">
       <div className="sw-flex sw-justify-between">
@@ -72,7 +67,7 @@ export default function ListHeader({ canCreate, refreshQualityGates }: Readonly<
           <HelperHintIcon />
         </DocumentationTooltip>
       </div>
-      {canCreate && <CreateQualityGateModal refreshQualityGates={refreshQualityGates} />}
+      {canCreate && <CreateQualityGateModal />}
     </div>
   );
 }
index 9d2c26bf6629501f928df09364d2695a84a92312..6fc406d4a9bd4dfd538cad06b4070c170de9867d 100644 (file)
  */
 import { ButtonPrimary, FormField, InputField, Modal } from 'design-system/lib';
 import * as React from 'react';
-import { renameQualityGate } from '../../../api/quality-gates';
-import { WithRouterProps, withRouter } from '../../../components/hoc/withRouter';
+import { useRouter } from '../../../components/hoc/withRouter';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
+import { useRenameQualityGateMutation } from '../../../queries/quality-gates';
 import { QualityGate } from '../../../types/types';
 
-interface Props extends WithRouterProps {
+interface Props {
   onClose: () => void;
-  onRename: () => Promise<void>;
   qualityGate: QualityGate;
 }
 
-interface State {
-  name: string;
-}
-
 const FORM_ID = 'rename-quality-gate';
 
-class RenameQualityGateForm extends React.PureComponent<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = { name: props.qualityGate.name };
-  }
+export default function RenameQualityGateForm({ qualityGate, onClose }: Readonly<Props>) {
+  const [name, setName] = React.useState(qualityGate.name);
+  const { mutateAsync: renameQualityGate } = useRenameQualityGateMutation(qualityGate.name);
+  const router = useRouter();
 
-  handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.setState({ name: event.currentTarget.value });
+  const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setName(event.currentTarget.value);
   };
 
-  handleRename = (event: React.FormEvent) => {
+  const handleRename = async (event: React.FormEvent) => {
     event.preventDefault();
 
-    const { qualityGate, router } = this.props;
-    const { name } = this.state;
-
-    return renameQualityGate({ currentName: qualityGate.name, name }).then(() => {
-      router.push(getQualityGateUrl(name));
-      this.props.onRename();
-    });
+    await renameQualityGate(name);
+    router.push(getQualityGateUrl(name));
   };
 
-  render() {
-    const { qualityGate } = this.props;
-    const { name } = this.state;
-    const confirmDisable = !name || (qualityGate && qualityGate.name === name);
+  const confirmDisable = !name || (qualityGate && qualityGate.name === name);
 
-    return (
-      <Modal
-        headerTitle={translate('quality_gates.rename')}
-        onClose={this.props.onClose}
-        body={
-          <form id={FORM_ID} onSubmit={this.handleRename}>
-            <MandatoryFieldsExplanation />
-            <FormField
-              label={translate('name')}
-              htmlFor="quality-gate-form-name"
-              required
-              className="sw-my-2"
-            >
-              <InputField
-                autoFocus
-                id="quality-gate-form-name"
-                maxLength={100}
-                onChange={this.handleNameChange}
-                size="auto"
-                type="text"
-                value={name}
-              />
-            </FormField>
-          </form>
-        }
-        primaryButton={
-          <ButtonPrimary autoFocus type="submit" disabled={confirmDisable} form={FORM_ID}>
-            {translate('rename')}
-          </ButtonPrimary>
-        }
-        secondaryButtonLabel={translate('cancel')}
-      />
-    );
-  }
+  return (
+    <Modal
+      headerTitle={translate('quality_gates.rename')}
+      onClose={onClose}
+      body={
+        <form id={FORM_ID} onSubmit={handleRename}>
+          <MandatoryFieldsExplanation />
+          <FormField
+            label={translate('name')}
+            htmlFor="quality-gate-form-name"
+            required
+            className="sw-my-2"
+          >
+            <InputField
+              autoFocus
+              id="quality-gate-form-name"
+              maxLength={100}
+              onChange={handleNameChange}
+              size="auto"
+              type="text"
+              value={name}
+            />
+          </FormField>
+        </form>
+      }
+      primaryButton={
+        <ButtonPrimary autoFocus type="submit" disabled={confirmDisable} form={FORM_ID}>
+          {translate('rename')}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
+  );
 }
-
-export default withRouter(RenameQualityGateForm);
index f6e563cc4225b6523be4ffc371d0b14b2ebfc9b9..90602910f361697fba5693a7ada29d27a35de59b 100644 (file)
@@ -28,6 +28,7 @@ import { mockLoggedInUser } from '../../../../helpers/testMocks';
 import { RenderContext, renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
 import { byRole } from '../../../../helpers/testSelector';
 import { Feature } from '../../../../types/features';
+import { CaycStatus } from '../../../../types/types';
 import { NoticeType } from '../../../../types/users';
 import routes from '../../routes';
 
@@ -140,7 +141,7 @@ it('should be able to copy a quality gate which is CAYC compliant', async () =>
 
   const notDefaultQualityGate = await screen.findByText('Sonar way');
   await user.click(notDefaultQualityGate);
-  await user.click(screen.getByLabelText('menu'));
+  await user.click(await screen.findByLabelText('menu'));
   const copyButton = screen.getByRole('menuitem', { name: 'copy' });
 
   await user.click(copyButton);
@@ -160,7 +161,7 @@ it('should not be able to copy a quality gate which is not CAYC compliant', asyn
 
   const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
   await user.click(notDefaultQualityGate);
-  await user.click(screen.getByLabelText('menu'));
+  await user.click(await screen.findByLabelText('menu'));
   const copyButton = screen.getByRole('menuitem', { name: 'copy' });
 
   expect(copyButton).toBeDisabled();
@@ -189,7 +190,7 @@ it('should not be able to set as default a quality gate which is not CAYC compli
 
   const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
   await user.click(notDefaultQualityGate);
-  await user.click(screen.getByLabelText('menu'));
+  await user.click(await screen.findByLabelText('menu'));
   const setAsDefaultButton = screen.getByRole('menuitem', { name: 'set_as_default' });
   expect(setAsDefaultButton).toBeDisabled();
 });
@@ -201,10 +202,10 @@ it('should be able to set as default a quality gate which is CAYC compliant', as
 
   const notDefaultQualityGate = await screen.findByRole('button', { name: /Sonar way/ });
   await user.click(notDefaultQualityGate);
-  await user.click(screen.getByLabelText('menu'));
+  await user.click(await screen.findByLabelText('menu'));
   const setAsDefaultButton = screen.getByRole('menuitem', { name: 'set_as_default' });
   await user.click(setAsDefaultButton);
-  expect(screen.getByRole('button', { name: /Sonar way default/ })).toBeInTheDocument();
+  expect(await screen.findByRole('button', { name: /Sonar way default/ })).toBeInTheDocument();
 });
 
 it('should be able to add a condition', async () => {
@@ -343,7 +344,7 @@ it('should show warning banner when CAYC condition is not properly set and shoul
 
   await user.click(qualityGate);
 
-  expect(screen.getByText('quality_gates.cayc_missing.banner.title')).toBeInTheDocument();
+  expect(await screen.findByText('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' }),
@@ -367,6 +368,8 @@ it('should show warning banner when CAYC condition is not properly set and shoul
     screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' }),
   ).toBeInTheDocument();
 
+  qualityGateHandler.setCaycStatusForQualityGate('SonarSource way - CFamily', CaycStatus.Compliant);
+
   await user.click(
     screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' }),
   );
@@ -388,7 +391,7 @@ it('should show optimize banner when CAYC condition is not properly set and QG i
 
   await user.click(qualityGate);
 
-  expect(screen.getByText('quality_gates.cayc_optimize.banner.title')).toBeInTheDocument();
+  expect(await screen.findByText('quality_gates.cayc_optimize.banner.title')).toBeInTheDocument();
   expect(screen.getByText('quality_gates.cayc_optimize.banner.description')).toBeInTheDocument();
   expect(
     screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_optimize' }),
@@ -559,7 +562,7 @@ describe('The Project section', () => {
     await user.click(notDefaultQualityGate);
 
     // by default it shows "selected" values
-    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+    expect(await screen.findAllByRole('checkbox')).toHaveLength(2);
 
     // change tabs to show deselected projects
     await user.click(screen.getByRole('radio', { name: 'quality_gates.projects.without' }));
@@ -579,8 +582,8 @@ describe('The Project section', () => {
 
     await user.click(notDefaultQualityGate);
 
+    expect(await screen.findAllByRole('checkbox')).toHaveLength(2);
     const checkedProjects = screen.getAllByRole('checkbox')[0];
-    expect(screen.getAllByRole('checkbox')).toHaveLength(2);
     await user.click(checkedProjects);
     const reloadButton = screen.getByRole('button', { name: 'reload' });
     expect(reloadButton).toBeInTheDocument();
@@ -610,7 +613,7 @@ describe('The Project section', () => {
 
     await user.click(notDefaultQualityGate);
 
-    const searchInput = screen.getByRole('searchbox', { name: 'search_verb' });
+    const searchInput = await screen.findByRole('searchbox', { name: 'search_verb' });
     expect(searchInput).toBeInTheDocument();
     await user.click(searchInput);
     await user.keyboard('test2{Enter}');
@@ -621,7 +624,7 @@ describe('The Project section', () => {
   });
 
   it('should display show more button if there are multiple pages of data', async () => {
-    (searchProjects as jest.Mock).mockResolvedValueOnce({
+    jest.mocked(searchProjects).mockResolvedValueOnce({
       paging: { pageIndex: 2, pageSize: 3, total: 55 },
       results: [],
     });
@@ -633,7 +636,7 @@ describe('The Project section', () => {
     const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
     await user.click(notDefaultQualityGate);
 
-    expect(screen.getByRole('button', { name: 'show_more' })).toBeInTheDocument();
+    expect(await screen.findByRole('button', { name: 'show_more' })).toBeInTheDocument();
   });
 });
 
index 3376cbb2267408a38fb479147fa0be6a5b9fb71d..a986757f42aee47e52c04f88dd3c54cefef349b4 100644 (file)
@@ -253,64 +253,6 @@ export function getCorrectCaycCondition(condition: Condition) {
   return OPTIMIZED_CAYC_CONDITIONS[conditionMetric];
 }
 
-export function addCondition(qualityGate: QualityGate, condition: Condition): QualityGate {
-  const oldConditions = qualityGate.conditions || [];
-  const conditions = [...oldConditions, condition];
-  if (conditions) {
-    qualityGate.caycStatus = updateCaycCompliantStatus(conditions);
-  }
-  return { ...qualityGate, conditions };
-}
-
-export function deleteCondition(qualityGate: QualityGate, condition: Condition): QualityGate {
-  const conditions =
-    qualityGate.conditions && qualityGate.conditions.filter((candidate) => candidate !== condition);
-  if (conditions) {
-    qualityGate.caycStatus = updateCaycCompliantStatus(conditions);
-  }
-  return { ...qualityGate, conditions };
-}
-
-export function replaceCondition(
-  qualityGate: QualityGate,
-  newCondition: Condition,
-  oldCondition: Condition,
-): QualityGate {
-  const conditions =
-    qualityGate.conditions &&
-    qualityGate.conditions.map((candidate) => {
-      return candidate === oldCondition ? newCondition : candidate;
-    });
-  if (conditions) {
-    qualityGate.caycStatus = updateCaycCompliantStatus(conditions);
-  }
-
-  return { ...qualityGate, conditions };
-}
-
-function updateCaycCompliantStatus(conditions: Condition[]) {
-  const isCompliantOptimized = Object.values(OPTIMIZED_CAYC_CONDITIONS).every((condition) => {
-    const foundCondition = conditions.find((c) => c.metric === condition.metric);
-    return (
-      foundCondition &&
-      !isWeakCondition(condition.metric as OptimizedCaycMetricKeys, foundCondition)
-    );
-  });
-  const isCompliantUnoptimized = Object.values(UNOPTIMIZED_CAYC_CONDITIONS).every((condition) => {
-    const foundCondition = conditions.find((c) => c.metric === condition.metric);
-    return (
-      foundCondition &&
-      !isWeakCondition(condition.metric as UnoptimizedCaycMetricKeys, foundCondition)
-    );
-  });
-
-  if (isCompliantOptimized || isCompliantUnoptimized) {
-    return CaycStatus.Compliant;
-  }
-
-  return CaycStatus.NonCompliant;
-}
-
 export function getPossibleOperators(metric: Metric) {
   if (metric.direction === 1) {
     return 'LT';
@@ -328,8 +270,8 @@ function getNoDiffMetric(metric: Metric, metrics: Dict<Metric>) {
   const regularMetricKey = metric.key.replace(/^new_/, '');
   if (isDiffMetric(metric.key) && metricKeyExists(regularMetricKey, metrics)) {
     return metrics[regularMetricKey];
-  } else if (metric.key === 'new_maintainability_rating') {
-    return metrics['sqale_rating'] || metric;
+  } else if (metric.key === MetricKey.new_maintainability_rating) {
+    return metrics[MetricKey.sqale_rating] || metric;
   }
   return metric;
 }
diff --git a/server/sonar-web/src/main/js/queries/quality-gates.ts b/server/sonar-web/src/main/js/queries/quality-gates.ts
new file mode 100644 (file)
index 0000000..79fc096
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+ * 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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import {
+  copyQualityGate,
+  createCondition,
+  createQualityGate,
+  deleteCondition,
+  deleteQualityGate,
+  fetchQualityGate,
+  fetchQualityGates,
+  renameQualityGate,
+  setQualityGateAsDefault,
+  updateCondition,
+} from '../api/quality-gates';
+import { getCorrectCaycCondition } from '../apps/quality-gates/utils';
+import { addGlobalSuccessMessage } from '../helpers/globalMessages';
+import { translate } from '../helpers/l10n';
+import { Condition, QualityGate } from '../types/types';
+
+const QUALITY_GATE_KEY = 'quality-gate';
+const QUALITY_GATES_KEY = 'quality-gates';
+
+export function useQualityGateQuery(name: string) {
+  return useQuery({
+    queryKey: [QUALITY_GATE_KEY, name] as const,
+    queryFn: ({ queryKey: [_, name] }) => {
+      return fetchQualityGate({ name });
+    },
+  });
+}
+
+export function useQualityGatesQuery() {
+  return useQuery({
+    queryKey: [QUALITY_GATES_KEY] as const,
+    queryFn: () => {
+      return fetchQualityGates();
+    },
+    staleTime: 2 * 60 * 1000,
+  });
+}
+
+export function useCreateQualityGateMutation() {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (name: string) => {
+      return createQualityGate({ name });
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+    },
+  });
+}
+
+export function useSetQualityGateAsDefaultMutation(gateName: string) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (qualityGate: QualityGate) => {
+      return setQualityGateAsDefault({ name: qualityGate.name });
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+      queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]);
+    },
+  });
+}
+
+export function useRenameQualityGateMutation(currentName: string) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (newName: string) => {
+      return renameQualityGate({ currentName, name: newName });
+    },
+    onSuccess: (_, newName: string) => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+      queryClient.invalidateQueries([QUALITY_GATE_KEY, newName]);
+    },
+  });
+}
+
+export function useCopyQualityGateMutation(sourceName: string) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (newName: string) => {
+      return copyQualityGate({ sourceName, name: newName });
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+    },
+  });
+}
+
+export function useDeleteQualityGateMutation(name: string) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: () => {
+      return deleteQualityGate({ name });
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+    },
+  });
+}
+
+export function useFixQualityGateMutation(gateName: string) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: ({
+      weakConditions,
+      missingConditions,
+    }: {
+      weakConditions: Condition[];
+      missingConditions: Condition[];
+    }) => {
+      const promiseArr = weakConditions
+        .map((condition) => {
+          return updateCondition({
+            ...getCorrectCaycCondition(condition),
+            id: condition.id,
+          });
+        })
+        .concat(
+          missingConditions.map((condition) => {
+            return createCondition({
+              ...getCorrectCaycCondition(condition),
+              gateName,
+            });
+          }),
+        );
+
+      return Promise.all(promiseArr);
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+      queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]);
+      addGlobalSuccessMessage(translate('quality_gates.conditions_updated'));
+    },
+  });
+}
+
+export function useCreateConditionMutation(gateName: string) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (condition: Omit<Condition, 'id'>) => {
+      return createCondition({ ...condition, gateName });
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+      queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]);
+      addGlobalSuccessMessage(translate('quality_gates.condition_added'));
+    },
+  });
+}
+
+export function useUpdateConditionMutation(gateName: string) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (condition: Condition) => {
+      return updateCondition(condition);
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+      queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]);
+      addGlobalSuccessMessage(translate('quality_gates.condition_updated'));
+    },
+  });
+}
+
+export function useDeleteConditionMutation(gateName: string) {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: (condition: Condition) => {
+      return deleteCondition({
+        id: condition.id,
+      });
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries([QUALITY_GATES_KEY]);
+      queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]);
+      addGlobalSuccessMessage(translate('quality_gates.condition_deleted'));
+    },
+  });
+}
index cab9fe1183e2dbd593aaf39e82640e45be754be2..c77e44ef4e596139ba114422315af069a620f713 100644 (file)
@@ -2164,6 +2164,7 @@ quality_gates.condition.delete=Delete condition on {0}
 quality_gates.condition_added=Successfully added condition.
 quality_gates.update_condition=Update Condition
 quality_gates.condition_updated=Successfully updated condition.
+quality_gates.conditions_updated=Successfully updated conditions.
 quality_gates.no_conditions=No Conditions
 quality_gates.health_icons=Project health icons represent:
 quality_gates.projects_for_default=Every project not specifically associated to a quality gate will be associated to this one by default.