]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10513 Remove redux store form quality gates app
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 8 May 2018 06:43:32 +0000 (08:43 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 8 May 2018 18:20:45 +0000 (20:20 +0200)
45 files changed:
server/sonar-web/src/main/js/api/quality-gates.ts
server/sonar-web/src/main/js/app/styles/components/menu.css
server/sonar-web/src/main/js/app/styles/init/type.css
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionSelect.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/Conditions.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx [new file with mode: 0644]
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/DeleteConditionForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Details.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Intro.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/Intro.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/List.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/containers/QualityGatesAppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/routes.ts
server/sonar-web/src/main/js/apps/quality-gates/store/actions.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/store/rootReducer.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/store/utils.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/SelectList/SelectListListElement.tsx
server/sonar-web/src/main/js/components/SelectList/__tests__/__snapshots__/SelectListListElement-test.tsx.snap
server/sonar-web/src/main/js/components/SelectList/styles.css
server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
server/sonar-web/src/main/js/store/rootReducer.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index fc72234b27e03a8d99b796e2ca1ca4c6c57fdda0..490b48c22b15192a55aa57ab07c4d7d3845f3691 100644 (file)
  */
 import { getJSON, post, postJSON } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
-import { Metric } from '../app/types';
-
-export interface ConditionBase {
-  error: string;
-  metric: string;
-  op?: string;
-  period?: number;
-  warning: string;
-}
-
-export interface Condition extends ConditionBase {
-  id: number;
-}
-
-export interface QualityGate {
-  actions?: {
-    associateProjects: boolean;
-    copy: boolean;
-    delete: boolean;
-    manageConditions: boolean;
-    rename: boolean;
-    setAsDefault: boolean;
-  };
-  conditions?: Condition[];
-  id: number;
-  isBuiltIn?: boolean;
-  isDefault?: boolean;
-  name: string;
-}
+import { Condition, Metric, QualityGate } from '../app/types';
 
 export function fetchQualityGates(data: {
   organization?: string;
@@ -106,7 +78,7 @@ export function createCondition(
   data: {
     gateId: number;
     organization?: string;
-  } & ConditionBase
+  } & Condition
 ): Promise<Condition> {
   return postJSON('/api/qualitygates/create_condition', data);
 }
@@ -136,10 +108,11 @@ export function getGateForProject(data: {
 export function searchGates(data: {
   gateId: number;
   organization?: string;
-  page: number;
-  pageSize: number;
-  selected: string;
-}): Promise<void | Response> {
+  page?: number;
+  pageSize?: number;
+  query?: string;
+  selected?: string;
+}): Promise<{ more: boolean; results: Array<{ id: string; name: string; selected: boolean }> }> {
   return getJSON('/api/qualitygates/search', data).catch(throwGlobalError);
 }
 
index 0ae00301aa39d2c82f2937e874338b18768ffd38..ac3fd543e5febeb7726d94bcce4b12cc0e36b813 100644 (file)
   pointer-events: none !important;
 }
 
+.menu > li > a.text-muted {
+  color: var(--secondFontColor);
+}
+
 .menu > li > a:hover,
 .menu > li > a:focus {
   text-decoration: none;
index f0c235042ca333b7e719d2d950dfacd0f1027d79..c150ffb4c7cd07881ab189a5cf8ddf255ba827a1 100644 (file)
@@ -246,7 +246,7 @@ small,
 }
 
 .text-muted {
-  color: var(--secondFontColor) !important;
+  color: var(--secondFontColor);
 }
 
 .text-muted-2 {
index 7d32fffa2ba17d3b1e02986a271e072c95e5a5f3..ffaf58534f1d684e5cbd17bee58328f4961db785 100644 (file)
@@ -82,6 +82,15 @@ interface ComponentConfiguration {
   showUpdateKey?: boolean;
 }
 
+export interface Condition {
+  error: string;
+  id?: number;
+  metric: string;
+  op?: string;
+  period?: number;
+  warning: string;
+}
+
 export interface CoveredFile {
   key: string;
   longName: string;
@@ -365,6 +374,22 @@ export interface PullRequest {
   url?: string;
 }
 
+export interface QualityGate {
+  actions?: {
+    associateProjects?: boolean;
+    copy?: boolean;
+    delete?: boolean;
+    manageConditions?: boolean;
+    rename?: boolean;
+    setAsDefault?: boolean;
+  };
+  conditions?: Condition[];
+  id: number;
+  isBuiltIn?: boolean;
+  isDefault?: boolean;
+  name: string;
+}
+
 export interface Rule {
   isTemplate?: boolean;
   key: string;
index 9d1d86e32dccb93bc769422dd243724f1e6b2fca..2ecc5f07a4f80940e37e08446e6da129d610b39d 100644 (file)
@@ -25,13 +25,12 @@ import {
   fetchQualityGates,
   getGateForProject,
   associateGateWithProject,
-  dissociateGateWithProject,
-  QualityGate
+  dissociateGateWithProject
 } from '../../api/quality-gates';
 import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
 import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
 import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
-import { Component } from '../../app/types';
+import { Component, QualityGate } from '../../app/types';
 import { translate } from '../../helpers/l10n';
 
 interface Props {
@@ -128,7 +127,7 @@ export default class App extends React.PureComponent<Props> {
     const { allGates, gate, loading } = this.state;
 
     return (
-      <div id="project-quality-gate" className="page page-limited">
+      <div className="page page-limited" id="project-quality-gate">
         <Suggestions suggestions="project_quality_gate" />
         <Helmet title={translate('project_quality_gate.page')} />
         <Header />
index 3a826687096578d5023aae72dbd0babc7216bd13..1ac3be588b2329088796f8fcf23213dd1dd50b5d 100644 (file)
@@ -18,9 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { QualityGate } from '../../api/quality-gates';
 import Select from '../../components/controls/Select';
 import { translate } from '../../helpers/l10n';
+import { QualityGate } from '../../app/types';
 
 interface Props {
   allGates: QualityGate[];
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js
deleted file mode 100644 (file)
index 03d0975..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { omitBy, map, sortBy } from 'lodash';
-import Select from '../../../components/controls/Select';
-import { translate, getLocalizedMetricName, getLocalizedMetricDomain } from '../../../helpers/l10n';
-
-export default function AddConditionForm({ metrics, onSelect }) {
-  function handleChange(option) {
-    const metric = option.value;
-
-    // e.target.value = '';
-    onSelect(metric);
-  }
-
-  const metricsToDisplay = omitBy(metrics, metric => metric.hidden);
-  const options = sortBy(
-    map(metricsToDisplay, metric => ({
-      value: metric.key,
-      label: getLocalizedMetricName(metric),
-      domain: metric.domain
-    })),
-    'domain'
-  );
-
-  // use "disabled" property to emulate optgroups
-  const optionsWithDomains = [];
-  options.forEach((option, index, options) => {
-    const previous = index > 0 ? options[index - 1] : null;
-    if (!previous || previous.domain !== option.domain) {
-      optionsWithDomains.push({
-        value: option.domain,
-        label: getLocalizedMetricDomain(option.domain),
-        disabled: true
-      });
-    }
-    optionsWithDomains.push(option);
-  });
-
-  return (
-    <div className="big-spacer-top panel bg-muted">
-      <Select
-        id="quality-gate-new-condition-metric"
-        className="text-middle input-large"
-        options={optionsWithDomains}
-        placeholder={translate('quality_gates.add_condition')}
-        onChange={handleChange}
-      />
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionSelect.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionSelect.tsx
new file mode 100644 (file)
index 0000000..ef67bfd
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { sortBy } from 'lodash';
+import Select from '../../../components/controls/Select';
+import { translate, getLocalizedMetricName, getLocalizedMetricDomain } from '../../../helpers/l10n';
+import { Metric } from '../../../app/types';
+
+interface Props {
+  metrics: Metric[];
+  onAddCondition: (metric: string) => void;
+}
+
+interface Option {
+  disabled?: boolean;
+  domain?: string;
+  label: string;
+  value: string;
+}
+
+export default class AddConditionSelect extends React.PureComponent<Props> {
+  handleChange = (option: Option) => {
+    this.props.onAddCondition(option.value);
+  };
+
+  render() {
+    const { metrics } = this.props;
+
+    const options: Option[] = sortBy(
+      metrics.map(metric => ({
+        value: metric.key,
+        label: getLocalizedMetricName(metric),
+        domain: metric.domain
+      })),
+      'domain'
+    );
+
+    // use "disabled" property to emulate optgroups
+    const optionsWithDomains: Option[] = [];
+    options.forEach((option, index, options) => {
+      const previous = index > 0 ? options[index - 1] : null;
+      if (option.domain && (!previous || previous.domain !== option.domain)) {
+        optionsWithDomains.push({
+          value: option.domain,
+          label: getLocalizedMetricDomain(option.domain),
+          disabled: true
+        });
+      }
+      optionsWithDomains.push(option);
+    });
+
+    return (
+      <div className="big-spacer-top panel bg-muted">
+        <Select
+          className="text-middle input-large"
+          onChange={this.handleChange}
+          options={optionsWithDomains}
+          placeholder={translate('quality_gates.add_condition')}
+        />
+      </div>
+    );
+  }
+}
index 2fd57fb40b896ff74e06fff19f813bf6f737cc61..e12692f12e3cdddd2a09d311b2544b32a5b74be1 100644 (file)
 import * as React from 'react';
 import DeleteConditionForm from './DeleteConditionForm';
 import ThresholdInput from './ThresholdInput';
-import {
-  Condition as ICondition,
-  ConditionBase,
-  createCondition,
-  QualityGate,
-  updateCondition
-} from '../../../api/quality-gates';
-import { Metric } from '../../../app/types';
+import { createCondition, updateCondition } from '../../../api/quality-gates';
+import { Condition as ICondition, Metric, QualityGate } from '../../../app/types';
 import Checkbox from '../../../components/controls/Checkbox';
 import Select from '../../../components/controls/Select';
 import { Button, ResetButtonLink } from '../../../components/ui/buttons';
 import { translate, getLocalizedMetricName } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
 
 interface Props {
   condition: ICondition;
-  edit: boolean;
+  canEdit: boolean;
   metric: Metric;
-  organization: string;
-  onDeleteCondition: (condition: ICondition) => void;
+  organization?: string;
+  onAddCondition: (metric: string) => void;
   onError: (error: any) => void;
+  onRemoveCondition: (Condition: ICondition) => void;
   onResetError: () => void;
-  onSaveCondition: (condition: ICondition, newCondition: ICondition) => void;
+  onSaveCondition: (newCondition: ICondition, oldCondition: ICondition) => void;
   qualityGate: QualityGate;
 }
 
 interface State {
   changed: boolean;
-  period?: number;
+  error: string;
   op?: string;
-  openDeleteCondition: boolean;
+  period?: number;
   warning: string;
-  error: string;
 }
 
 export default class Condition extends React.PureComponent<Props, State> {
@@ -62,41 +56,45 @@ export default class Condition extends React.PureComponent<Props, State> {
       changed: false,
       period: props.condition.period,
       op: props.condition.op,
-      openDeleteCondition: false,
       warning: props.condition.warning || '',
       error: props.condition.error || ''
     };
   }
 
-  handleOperatorChange = ({ value }: any) => this.setState({ changed: true, op: value });
-
-  handlePeriodChange = (checked: boolean) => {
-    const period = checked ? 1 : undefined;
-    this.setState({ changed: true, period });
-  };
-
-  handleWarningChange = (warning: string) => this.setState({ changed: true, warning });
-
-  handleErrorChange = (error: string) => this.setState({ changed: true, error });
-
-  handleSaveClick = () => {
-    const { qualityGate, condition, metric, organization } = this.props;
-    const { period } = this.state;
-    const data: ConditionBase = {
-      metric: condition.metric,
+  getUpdatedCondition = () => {
+    const { metric } = this.props;
+    const data: ICondition = {
+      metric: metric.key,
       op: metric.type === 'RATING' ? 'GT' : this.state.op,
       warning: this.state.warning,
       error: this.state.error
     };
 
+    const { period } = this.state;
     if (period && metric.type !== 'RATING') {
       data.period = period;
     }
 
-    if (metric.key.indexOf('new_') === 0) {
+    if (isDiffMetric(metric.key)) {
       data.period = 1;
     }
+    return data;
+  };
+
+  handleOperatorChange = ({ value }: any) => this.setState({ changed: true, op: value });
+
+  handlePeriodChange = (checked: boolean) => {
+    const period = checked ? 1 : undefined;
+    this.setState({ changed: true, period });
+  };
+
+  handleWarningChange = (warning: string) => this.setState({ changed: true, warning });
 
+  handleErrorChange = (error: string) => this.setState({ changed: true, error });
+
+  handleSaveClick = () => {
+    const { qualityGate, organization } = this.props;
+    const data = this.getUpdatedCondition();
     createCondition({ gateId: qualityGate.id, organization, ...data }).then(
       this.handleConditionResponse,
       this.props.onError
@@ -104,24 +102,12 @@ export default class Condition extends React.PureComponent<Props, State> {
   };
 
   handleUpdateClick = () => {
-    const { condition, metric, organization } = this.props;
-    const { period } = this.state;
+    const { condition, organization } = this.props;
     const data: ICondition = {
       id: condition.id,
-      metric: condition.metric,
-      op: metric.type === 'RATING' ? 'GT' : this.state.op,
-      warning: this.state.warning,
-      error: this.state.error
+      ...this.getUpdatedCondition()
     };
 
-    if (period && metric.type !== 'RATING') {
-      data.period = period;
-    }
-
-    if (metric.key.indexOf('new_') === 0) {
-      data.period = 1;
-    }
-
     updateCondition({ organization, ...data }).then(
       this.handleConditionResponse,
       this.props.onError
@@ -129,30 +115,21 @@ export default class Condition extends React.PureComponent<Props, State> {
   };
 
   handleConditionResponse = (newCondition: ICondition) => {
-    this.setState({ changed: false });
-    this.props.onSaveCondition(this.props.condition, newCondition);
+    this.props.onSaveCondition(newCondition, this.props.condition);
     this.props.onResetError();
+    this.setState({ changed: false });
   };
 
   handleCancelClick = () => {
-    this.props.onDeleteCondition(this.props.condition);
-  };
-
-  openDeleteConditionForm = () => {
-    this.setState({ openDeleteCondition: true });
-  };
-
-  closeDeleteConditionForm = () => {
-    this.setState({ openDeleteCondition: false });
+    this.props.onRemoveCondition(this.props.condition);
   };
 
   renderPeriodValue() {
     const { condition, metric } = this.props;
     const isLeakSelected = !!this.state.period;
-    const isDiffMetric = condition.metric.indexOf('new_') === 0;
     const isRating = metric.type === 'RATING';
 
-    if (isDiffMetric) {
+    if (isDiffMetric(condition.metric)) {
       return (
         <span className="note">{translate('quality_gates.condition.leak.unconditional')}</span>
       );
@@ -168,13 +145,11 @@ export default class Condition extends React.PureComponent<Props, State> {
   }
 
   renderPeriod() {
-    const { condition, metric, edit } = this.props;
-
-    const isDiffMetric = condition.metric.indexOf('new_') === 0;
+    const { condition, metric, canEdit } = this.props;
     const isRating = metric.type === 'RATING';
     const isLeakSelected = !!this.state.period;
 
-    if (isRating || isDiffMetric || !edit) {
+    if (isRating || isDiffMetric(condition.metric) || !canEdit) {
       return this.renderPeriodValue();
     }
 
@@ -182,9 +157,9 @@ export default class Condition extends React.PureComponent<Props, State> {
   }
 
   renderOperator() {
-    const { condition, edit, metric } = this.props;
+    const { condition, canEdit, metric } = this.props;
 
-    if (!edit && condition.op) {
+    if (!canEdit && condition.op) {
       return metric.type === 'RATING'
         ? translate('quality_gates.operator', condition.op, 'rating')
         : translate('quality_gates.operator', condition.op);
@@ -215,7 +190,7 @@ export default class Condition extends React.PureComponent<Props, State> {
   }
 
   render() {
-    const { condition, edit, metric, organization } = this.props;
+    const { condition, canEdit, metric, organization } = this.props;
     return (
       <tr>
         <td className="text-middle">
@@ -230,7 +205,7 @@ export default class Condition extends React.PureComponent<Props, State> {
         <td className="thin text-middle nowrap">{this.renderOperator()}</td>
 
         <td className="thin text-middle nowrap">
-          {edit ? (
+          {canEdit ? (
             <ThresholdInput
               metric={metric}
               name="warning"
@@ -243,7 +218,7 @@ export default class Condition extends React.PureComponent<Props, State> {
         </td>
 
         <td className="thin text-middle nowrap">
-          {edit ? (
+          {canEdit ? (
             <ThresholdInput
               metric={metric}
               name="error"
@@ -255,7 +230,7 @@ export default class Condition extends React.PureComponent<Props, State> {
           )}
         </td>
 
-        {edit && (
+        {canEdit && (
           <td className="thin text-middle nowrap">
             {condition.id ? (
               <div>
@@ -265,20 +240,12 @@ export default class Condition extends React.PureComponent<Props, State> {
                   onClick={this.handleUpdateClick}>
                   {translate('update_verb')}
                 </Button>
-                <Button
-                  className="button-red delete-condition little-spacer-left"
-                  onClick={this.openDeleteConditionForm}>
-                  {translate('delete')}
-                </Button>
-                {this.state.openDeleteCondition && (
-                  <DeleteConditionForm
-                    condition={condition}
-                    metric={metric}
-                    onClose={this.closeDeleteConditionForm}
-                    onDelete={this.props.onDeleteCondition}
-                    organization={organization}
-                  />
-                )}
+                <DeleteConditionForm
+                  condition={condition}
+                  metric={metric}
+                  onDelete={this.props.onRemoveCondition}
+                  organization={organization}
+                />
               </div>
             ) : (
               <div>
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js
deleted file mode 100644 (file)
index 982d6d9..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { sortBy, uniqBy } from 'lodash';
-import AddConditionForm from './AddConditionForm';
-import Condition from './Condition';
-import DocTooltip from '../../../components/docs/DocTooltip';
-import { translate, getLocalizedMetricName } from '../../../helpers/l10n';
-
-function getKey(condition, index) {
-  return condition.id ? condition.id : `new-${index}`;
-}
-
-export default class Conditions extends React.PureComponent {
-  state = {
-    error: null
-  };
-
-  componentWillUpdate(nextProps) {
-    if (nextProps.qualityGate !== this.props.qualityGate) {
-      this.setState({ error: null });
-    }
-  }
-
-  handleError(error) {
-    try {
-      error.response.json().then(r => {
-        const message = r.errors.map(e => e.msg).join('. ');
-        this.setState({ error: message });
-      });
-    } catch (ex) {
-      this.setState({ error: translate('default_error_message') });
-    }
-  }
-
-  handleResetError() {
-    this.setState({ error: null });
-  }
-
-  render() {
-    const {
-      qualityGate,
-      conditions,
-      metrics,
-      edit,
-      onAddCondition,
-      onSaveCondition,
-      onDeleteCondition,
-      organization
-    } = this.props;
-
-    const existingConditions = conditions.filter(condition => metrics[condition.metric]);
-
-    const sortedConditions = sortBy(
-      existingConditions,
-      condition => metrics[condition.metric] && metrics[condition.metric].name
-    );
-
-    const duplicates = [];
-    const savedConditions = existingConditions.filter(condition => condition.id != null);
-    savedConditions.forEach(condition => {
-      const sameCount = savedConditions.filter(
-        sample => sample.metric === condition.metric && sample.period === condition.period
-      ).length;
-      if (sameCount > 1) {
-        duplicates.push(condition);
-      }
-    });
-
-    const uniqDuplicates = uniqBy(duplicates, d => d.metric).map(condition => ({
-      ...condition,
-      metric: metrics[condition.metric]
-    }));
-    return (
-      <div className="quality-gate-section" id="quality-gate-conditions">
-        <header className="display-flex-center spacer-bottom">
-          <h3>{translate('quality_gates.conditions')}</h3>
-          <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-conditions" />
-        </header>
-
-        <div className="big-spacer-bottom">{translate('quality_gates.introduction')}</div>
-
-        {this.state.error && <div className="alert alert-danger">{this.state.error}</div>}
-
-        {uniqDuplicates.length > 0 && (
-          <div className="alert alert-warning">
-            <p>{translate('quality_gates.duplicated_conditions')}</p>
-            <ul className="list-styled spacer-top">
-              {uniqDuplicates.map(d => (
-                <li key={d.metric.key}>{getLocalizedMetricName(d.metric)}</li>
-              ))}
-            </ul>
-          </div>
-        )}
-
-        {sortedConditions.length ? (
-          <table className="data zebra zebra-hover" id="quality-gate-conditions">
-            <thead>
-              <tr>
-                <th className="nowrap">{translate('quality_gates.conditions.metric')}</th>
-                <th className="thin nowrap">{translate('quality_gates.conditions.leak')}</th>
-                <th className="thin nowrap">{translate('quality_gates.conditions.operator')}</th>
-                <th className="thin nowrap">{translate('quality_gates.conditions.warning')}</th>
-                <th className="thin nowrap">{translate('quality_gates.conditions.error')}</th>
-                {edit && <th />}
-              </tr>
-            </thead>
-            <tbody>
-              {sortedConditions.map((condition, index) => (
-                <Condition
-                  key={getKey(condition, index)}
-                  qualityGate={qualityGate}
-                  condition={condition}
-                  metric={metrics[condition.metric]}
-                  edit={edit}
-                  onSaveCondition={onSaveCondition}
-                  onDeleteCondition={onDeleteCondition}
-                  onError={this.handleError.bind(this)}
-                  onResetError={this.handleResetError.bind(this)}
-                  organization={organization}
-                />
-              ))}
-            </tbody>
-          </table>
-        ) : (
-          <div className="big-spacer-top">{translate('quality_gates.no_conditions')}</div>
-        )}
-
-        {edit && <AddConditionForm metrics={metrics} onSelect={onAddCondition} />}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
new file mode 100644 (file)
index 0000000..0df36b5
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { differenceWith, map, sortBy, uniqBy } from 'lodash';
+import AddConditionSelect from './AddConditionSelect';
+import Condition from './Condition';
+import DocTooltip from '../../../components/docs/DocTooltip';
+import { translate, getLocalizedMetricName } from '../../../helpers/l10n';
+import { Condition as ICondition, Metric, QualityGate } from '../../../app/types';
+import { parseError } from '../../../helpers/request';
+
+interface Props {
+  canEdit: boolean;
+  conditions: ICondition[];
+  metrics: { [key: string]: Metric };
+  onAddCondition: (metric: string) => void;
+  onSaveCondition: (newCondition: ICondition, oldCondition: ICondition) => void;
+  onRemoveCondition: (Condition: ICondition) => void;
+  organization?: string;
+  qualityGate: QualityGate;
+}
+
+interface State {
+  error?: string;
+}
+
+export default class Conditions extends React.PureComponent<Props, State> {
+  state: State = {};
+
+  componentWillUpdate(nextProps: Props) {
+    if (nextProps.qualityGate !== this.props.qualityGate) {
+      this.setState({ error: undefined });
+    }
+  }
+
+  getConditionKey = (condition: ICondition, index: number) => {
+    return condition.id ? condition.id : `new-${index}`;
+  };
+
+  handleError = (error: any) => {
+    parseError(error).then(
+      message => {
+        this.setState({ error: message });
+      },
+      () => {}
+    );
+  };
+
+  handleResetError = () => {
+    this.setState({ error: undefined });
+  };
+
+  render() {
+    const { qualityGate, conditions, metrics, canEdit, organization } = this.props;
+
+    const existingConditions = conditions.filter(condition => metrics[condition.metric]);
+
+    const sortedConditions = sortBy(
+      existingConditions,
+      condition => metrics[condition.metric] && metrics[condition.metric].name
+    );
+
+    const duplicates: ICondition[] = [];
+    const savedConditions = existingConditions.filter(condition => condition.id != null);
+    savedConditions.forEach(condition => {
+      const sameCount = savedConditions.filter(
+        sample => sample.metric === condition.metric && sample.period === condition.period
+      ).length;
+      if (sameCount > 1) {
+        duplicates.push(condition);
+      }
+    });
+
+    const uniqDuplicates = uniqBy(duplicates, d => d.metric).map(condition => ({
+      ...condition,
+      metric: metrics[condition.metric]
+    }));
+
+    const availableMetrics = differenceWith(
+      map(metrics, metric => metric).filter(
+        metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type)
+      ),
+      conditions,
+      (metric, condition) => metric.key === condition.metric
+    );
+
+    return (
+      <div className="quality-gate-section" id="quality-gate-conditions">
+        <header className="display-flex-center spacer-bottom">
+          <h3>{translate('quality_gates.conditions')}</h3>
+          <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-conditions" />
+        </header>
+
+        <div className="big-spacer-bottom">{translate('quality_gates.introduction')}</div>
+
+        {this.state.error && <div className="alert alert-danger">{this.state.error}</div>}
+
+        {uniqDuplicates.length > 0 && (
+          <div className="alert alert-warning">
+            <p>{translate('quality_gates.duplicated_conditions')}</p>
+            <ul className="list-styled spacer-top">
+              {uniqDuplicates.map(d => (
+                <li key={d.metric.key}>{getLocalizedMetricName(d.metric)}</li>
+              ))}
+            </ul>
+          </div>
+        )}
+
+        {sortedConditions.length ? (
+          <table className="data zebra zebra-hover" id="quality-gate-conditions">
+            <thead>
+              <tr>
+                <th className="nowrap">{translate('quality_gates.conditions.metric')}</th>
+                <th className="thin nowrap">{translate('quality_gates.conditions.leak')}</th>
+                <th className="thin nowrap">{translate('quality_gates.conditions.operator')}</th>
+                <th className="thin nowrap">{translate('quality_gates.conditions.warning')}</th>
+                <th className="thin nowrap">{translate('quality_gates.conditions.error')}</th>
+                {canEdit && <th />}
+              </tr>
+            </thead>
+            <tbody>
+              {sortedConditions.map((condition, index) => (
+                <Condition
+                  canEdit={canEdit}
+                  condition={condition}
+                  key={this.getConditionKey(condition, index)}
+                  metric={metrics[condition.metric]}
+                  onAddCondition={this.props.onAddCondition}
+                  onError={this.handleError}
+                  onRemoveCondition={this.props.onRemoveCondition}
+                  onResetError={this.handleResetError}
+                  onSaveCondition={this.props.onSaveCondition}
+                  organization={organization}
+                  qualityGate={qualityGate}
+                />
+              ))}
+            </tbody>
+          </table>
+        ) : (
+          <div className="big-spacer-top">{translate('quality_gates.no_conditions')}</div>
+        )}
+
+        {canEdit && (
+          <AddConditionSelect
+            metrics={availableMetrics}
+            onAddCondition={this.props.onAddCondition}
+          />
+        )}
+      </div>
+    );
+  }
+}
index 7583b76de78811459ed41d4b86594f002b8661c5..656dd635ee4bc78a3b8e90642cf43266afbfc183 100644 (file)
  */
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
-import { copyQualityGate, QualityGate } from '../../../api/quality-gates';
-import Modal from '../../../components/controls/Modal';
-import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons';
+import { copyQualityGate } from '../../../api/quality-gates';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { Button } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
+import { QualityGate } from '../../../app/types';
 
 interface Props {
-  qualityGate: QualityGate;
-  onCopy: (newQualityGate: QualityGate) => void;
-  onClose: () => void;
+  onCopy: () => Promise<void>;
   organization?: string;
+  qualityGate: QualityGate;
 }
 
 interface State {
-  loading: boolean;
   name: string;
 }
 
 export default class CopyQualityGateForm extends React.PureComponent<Props, State> {
-  mounted = false;
-
   static contextTypes = {
     router: PropTypes.object
   };
 
   constructor(props: Props) {
     super(props);
-    this.state = { loading: false, name: props.qualityGate.name };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
+    this.state = { name: props.qualityGate.name };
   }
 
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+  handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     this.setState({ name: event.currentTarget.value });
   };
 
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
+  onCopy = () => {
     const { qualityGate, organization } = this.props;
     const { name } = this.state;
-    if (name) {
-      this.setState({ loading: true });
-      copyQualityGate({ id: qualityGate.id, name, organization }).then(
-        qualityGate => {
-          this.props.onCopy(qualityGate);
-          this.props.onClose();
-          this.context.router.push(
-            getQualityGateUrl(String(qualityGate.id), this.props.organization)
-          );
-        },
-        () => {
-          if (this.mounted) {
-            this.setState({ loading: false });
-          }
-        }
-      );
+
+    if (!name) {
+      return undefined;
     }
+
+    return copyQualityGate({ id: qualityGate.id, name, organization }).then(qualityGate => {
+      this.props.onCopy();
+      this.context.router.push(getQualityGateUrl(String(qualityGate.id), this.props.organization));
+    });
   };
 
   render() {
     const { qualityGate } = this.props;
-    const { loading, name } = this.state;
-    const header = translate('quality_gates.copy');
-    const submitDisabled = loading || !name || (qualityGate && qualityGate.name === name);
+    const { name } = this.state;
+    const confirmDisable = !name || (qualityGate && qualityGate.name === name);
 
     return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form id="quality-gate-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
+      <ConfirmButton
+        confirmButtonText={translate('copy')}
+        confirmDisable={confirmDisable}
+        modalBody={
+          <div className="modal-field">
+            <label htmlFor="quality-gate-form-name">
+              {translate('name')}
+              <em className="mandatory">*</em>
+            </label>
+            <input
+              autoFocus={true}
+              id="quality-gate-form-name"
+              maxLength={100}
+              onChange={this.handleNameChange}
+              required={true}
+              size={50}
+              type="text"
+              value={name}
+            />
           </div>
-          <div className="modal-body">
-            <div className="modal-field">
-              <label htmlFor="quality-gate-form-name">
-                {translate('name')}
-                <em className="mandatory">*</em>
-              </label>
-              <input
-                autoFocus={true}
-                id="quality-gate-form-name"
-                maxLength={100}
-                onChange={this.handleNameChange}
-                required={true}
-                size={50}
-                type="text"
-                value={name}
-              />
-            </div>
-          </div>
-          <div className="modal-foot">
-            {loading && <i className="spinner spacer-right" />}
-            <SubmitButton className="js-confirm" disabled={submitDisabled}>
-              {translate('copy')}
-            </SubmitButton>
-            <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
+        }
+        modalHeader={translate('quality_gates.copy')}
+        onConfirm={this.onCopy}>
+        {({ onClick }) => (
+          <Button className="little-spacer-left" id="quality-gate-copy" onClick={onClick}>
+            {translate('copy')}
+          </Button>
+        )}
+      </ConfirmButton>
     );
   }
 }
index 6a0ca705301f0fc70dc59fcb169b0b0437813f18..b984d9629419ad96fddaa23a8945b8230b9cb6c6 100644 (file)
  */
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
-import { createQualityGate, QualityGate } from '../../../api/quality-gates';
-import Modal from '../../../components/controls/Modal';
-import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { createQualityGate } from '../../../api/quality-gates';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { Button } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
 
 interface Props {
-  onCreate: (qualityGate: QualityGate) => void;
-  onClose: () => void;
+  onCreate: () => Promise<void>;
   organization?: string;
 }
 
 interface State {
-  loading: boolean;
   name: string;
 }
 
 export default class CreateQualityGateForm extends React.PureComponent<Props, State> {
-  mounted = false;
-
   static contextTypes = {
     router: PropTypes.object
   };
 
-  state = { loading: false, name: '' };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  state = { name: '' };
 
   handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
     this.setState({ name: event.currentTarget.value });
   };
 
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
+  handleCreate = () => {
     const { organization } = this.props;
     const { name } = this.state;
-    if (name) {
-      this.setState({ loading: true });
-      createQualityGate({ name, organization }).then(
-        qualityGate => {
-          this.props.onCreate(qualityGate);
-          this.context.router.push(getQualityGateUrl(String(qualityGate.id), organization));
-          this.props.onClose();
-        },
-        () => {
-          if (this.mounted) {
-            this.setState({ loading: false });
-          }
-        }
-      );
+
+    if (!name) {
+      return undefined;
     }
+
+    return createQualityGate({ name, organization })
+      .then(qualityGate => {
+        return this.props.onCreate().then(() => qualityGate);
+      })
+      .then(qualityGate => {
+        this.context.router.push(getQualityGateUrl(String(qualityGate.id), organization));
+      });
   };
 
   render() {
-    const { loading, name } = this.state;
-    const header = translate('quality_gates.create');
-    const submitDisabled = loading || !name;
-
+    const { name } = this.state;
     return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form id="quality-gate-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
+      <ConfirmButton
+        confirmButtonText={translate('save')}
+        confirmDisable={!name}
+        modalBody={
+          <div className="modal-field">
+            <label htmlFor="quality-gate-form-name">
+              {translate('name')}
+              <em className="mandatory">*</em>
+            </label>
+            <input
+              autoFocus={true}
+              id="quality-gate-form-name"
+              maxLength={100}
+              onChange={this.handleNameChange}
+              required={true}
+              size={50}
+              type="text"
+              value={name}
+            />
           </div>
-          <div className="modal-body">
-            <div className="modal-field">
-              <label htmlFor="quality-gate-form-name">
-                {translate('name')}
-                <em className="mandatory">*</em>
-              </label>
-              <input
-                autoFocus={true}
-                id="quality-gate-form-name"
-                maxLength={100}
-                onChange={this.handleNameChange}
-                required={true}
-                size={50}
-                type="text"
-                value={name}
-              />
-            </div>
-          </div>
-          <div className="modal-foot">
-            {loading && <i className="spinner spacer-right" />}
-            <SubmitButton className="js-confirm" disabled={submitDisabled}>
-              {translate('save')}
-            </SubmitButton>
-            <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
+        }
+        modalHeader={translate('quality_gates.create')}
+        onConfirm={this.handleCreate}>
+        {({ onClick }) => (
+          <Button id="quality-gate-add" onClick={onClick}>
+            {translate('create')}
+          </Button>
+        )}
+      </ConfirmButton>
     );
   }
 }
index ce41277c1ad44ab523a69143dd3f8e7bfa023a8b..3b52e3a8f5fe190580fee327a65824db6dedd325 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Condition, deleteCondition } from '../../../api/quality-gates';
-import { Metric } from '../../../app/types';
-import Modal from '../../../components/controls/Modal';
-import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { deleteCondition } from '../../../api/quality-gates';
+import { Metric, Condition } from '../../../app/types';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { Button } from '../../../components/ui/buttons';
 import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
 
 interface Props {
   condition: Condition;
   metric: Metric;
-  onClose: () => void;
   onDelete: (condition: Condition) => void;
   organization?: string;
 }
 
-interface State {
-  loading: boolean;
-}
-
-export default class DeleteConditionForm extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
+export default class DeleteConditionForm extends React.PureComponent<Props> {
+  onDelete = () => {
     const { organization, condition } = this.props;
-    this.setState({ loading: true });
-    deleteCondition({ id: condition.id, organization }).then(
-      () => this.props.onDelete(condition),
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
+    if (condition.id !== undefined) {
+      return deleteCondition({ id: condition.id, organization }).then(() =>
+        this.props.onDelete(condition)
+      );
+    }
+    return undefined;
   };
 
   render() {
-    const { metric } = this.props;
-    const header = translate('quality_gates.delete_condition');
-
     return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form id="delete-profile-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
-          </div>
-          <div className="modal-body">
-            <p>
-              {translateWithParameters(
-                'quality_gates.delete_condition.confirm.message',
-                getLocalizedMetricName(metric)
-              )}
-            </p>
-          </div>
-          <div className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            <SubmitButton className="js-delete button-red" disabled={this.state.loading}>
-              {translate('delete')}
-            </SubmitButton>
-            <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
+      <ConfirmButton
+        confirmButtonText={translate('delete')}
+        isDestructive={true}
+        modalBody={translateWithParameters(
+          'quality_gates.delete_condition.confirm.message',
+          getLocalizedMetricName(this.props.metric)
+        )}
+        modalHeader={translate('quality_gates.delete_condition')}
+        onConfirm={this.onDelete}>
+        {({ onClick }) => (
+          <Button className="delete-condition little-spacer-left button-red" onClick={onClick}>
+            {translate('delete')}
+          </Button>
+        )}
+      </ConfirmButton>
     );
   }
 }
index 1d0edec416ab1b4194895a1c00d7c9aeaf90644b..818b2d6d052223cb3f74b1793d6b111d79f6abba 100644 (file)
  */
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
-import { deleteQualityGate, QualityGate } from '../../../api/quality-gates';
-import Modal from '../../../components/controls/Modal';
-import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { deleteQualityGate } from '../../../api/quality-gates';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { Button } from '../../../components/ui/buttons';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getQualityGatesUrl } from '../../../helpers/urls';
+import { QualityGate } from '../../../app/types';
 
 interface Props {
-  onClose: () => void;
-  onDelete: (qualityGate: QualityGate) => void;
+  onDelete: () => Promise<void>;
   organization?: string;
   qualityGate: QualityGate;
 }
 
-interface State {
-  loading: boolean;
-}
-
-export default class DeleteQualityGateForm extends React.PureComponent<Props, State> {
-  mounted = false;
-
+export default class DeleteQualityGateForm extends React.PureComponent<Props> {
   static contextTypes = {
     router: PropTypes.object
   };
 
-  state: State = { loading: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
+  onDelete = () => {
     const { organization, qualityGate } = this.props;
-    this.setState({ loading: true });
-    deleteQualityGate({ id: qualityGate.id, organization }).then(
-      () => {
-        this.props.onDelete(qualityGate);
-        this.context.router.replace(getQualityGatesUrl(organization));
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
+    return deleteQualityGate({ id: qualityGate.id, organization })
+      .then(this.props.onDelete)
+      .then(() => {
+        this.context.router.push(getQualityGatesUrl(organization));
+      });
   };
 
   render() {
     const { qualityGate } = this.props;
-    const header = translate('quality_gates.delete');
 
     return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form id="delete-profile-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
-          </div>
-          <div className="modal-body">
-            <p>
-              {translateWithParameters('quality_gates.delete.confirm.message', qualityGate.name)}
-            </p>
-          </div>
-          <div className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            <SubmitButton className="js-delete button-red" disabled={this.state.loading}>
-              {translate('delete')}
-            </SubmitButton>
-            <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
+      <ConfirmButton
+        confirmButtonText={translate('delete')}
+        isDestructive={true}
+        modalBody={translateWithParameters(
+          'quality_gates.delete.confirm.message',
+          qualityGate.name
+        )}
+        modalHeader={translate('quality_gates.delete')}
+        onConfirm={this.onDelete}>
+        {({ onClick }) => (
+          <Button
+            className="little-spacer-left button-red"
+            id="quality-gate-delete"
+            onClick={onClick}>
+            {translate('delete')}
+          </Button>
+        )}
+      </ConfirmButton>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js
deleted file mode 100644 (file)
index 37c9627..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import Helmet from 'react-helmet';
-import DetailsHeader from './DetailsHeader';
-import DetailsContent from './DetailsContent';
-import { fetchQualityGate } from '../../../api/quality-gates';
-
-export default class Details extends React.PureComponent {
-  static contextTypes = {
-    router: PropTypes.object.isRequired
-  };
-
-  componentDidMount() {
-    this.props.fetchMetrics();
-    this.fetchDetails();
-  }
-
-  componentDidUpdate(prevProps) {
-    if (prevProps.params.id !== this.props.params.id) {
-      this.fetchDetails();
-    }
-  }
-
-  fetchDetails = () =>
-    fetchQualityGate({
-      id: this.props.params.id,
-      organization: this.props.organization && this.props.organization.key
-    }).then(qualityGate => this.props.onShow(qualityGate), () => {});
-
-  render() {
-    const { organization, metrics, qualityGate } = this.props;
-    const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props;
-
-    if (!qualityGate) {
-      return null;
-    }
-
-    return (
-      <div className="layout-page-main">
-        <Helmet title={qualityGate.name} />
-        <DetailsHeader
-          qualityGate={qualityGate}
-          onRename={this.props.onRename}
-          onCopy={this.props.onCopy}
-          onSetAsDefault={this.props.onSetAsDefault}
-          onDelete={this.props.onDelete}
-          organization={organization && organization.key}
-        />
-
-        <DetailsContent
-          gate={qualityGate}
-          metrics={metrics}
-          onAddCondition={onAddCondition}
-          onSaveCondition={onSaveCondition}
-          onDeleteCondition={onDeleteCondition}
-          organization={organization && organization.key}
-        />
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx
new file mode 100644 (file)
index 0000000..4c01648
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import * as PropTypes from 'prop-types';
+import Helmet from 'react-helmet';
+import { connect } from 'react-redux';
+import DetailsHeader from './DetailsHeader';
+import DetailsContent from './DetailsContent';
+import { getMetrics } from '../../../store/rootReducer';
+import { fetchMetrics } from '../../../store/rootActions';
+import { fetchQualityGate } from '../../../api/quality-gates';
+import { Metric, QualityGate, Condition } from '../../../app/types';
+import { checkIfDefault, addCondition, replaceCondition, deleteCondition } from '../utils';
+
+interface OwnProps {
+  onSetDefault: (qualityGate: QualityGate) => void;
+  organization?: string;
+  params: { id: number };
+  qualityGates: QualityGate[];
+  refreshQualityGates: () => Promise<void>;
+}
+
+interface StateToProps {
+  metrics: { [key: string]: Metric };
+}
+
+interface DispatchToProps {
+  fetchMetrics: () => void;
+}
+
+type Props = StateToProps & DispatchToProps & OwnProps;
+
+interface State {
+  loading: boolean;
+  qualityGate?: QualityGate;
+}
+
+export class DetailsApp extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  static contextTypes = {
+    router: PropTypes.object.isRequired
+  };
+
+  state: State = { loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.props.fetchMetrics();
+    this.fetchDetails();
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (nextProps.params.id !== this.props.params.id) {
+      this.setState({ loading: true });
+      this.fetchDetails(nextProps);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchDetails = ({ organization, params } = this.props) => {
+    return fetchQualityGate({ id: params.id, organization }).then(
+      qualityGate => {
+        if (this.mounted) {
+          this.setState({ loading: false, qualityGate });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleAddCondition = (metric: string) => {
+    this.setState(({ qualityGate }) => {
+      if (!qualityGate) {
+        return undefined;
+      }
+      return { qualityGate: addCondition(qualityGate, metric) };
+    });
+  };
+
+  handleSaveCondition = (newCondition: Condition, oldCondition: Condition) => {
+    this.setState(({ qualityGate }) => {
+      if (!qualityGate) {
+        return undefined;
+      }
+      return { qualityGate: replaceCondition(qualityGate, newCondition, oldCondition) };
+    });
+  };
+
+  handleRemoveCondition = (condition: Condition) => {
+    this.setState(({ qualityGate }) => {
+      if (!qualityGate) {
+        return undefined;
+      }
+      return { qualityGate: deleteCondition(qualityGate, condition) };
+    });
+  };
+
+  handleSetDefault = () => {
+    this.setState(({ qualityGate }) => {
+      if (!qualityGate) {
+        return undefined;
+      }
+      this.props.onSetDefault(qualityGate);
+      const newQualityGate: QualityGate = {
+        ...qualityGate,
+        actions: { ...qualityGate.actions, delete: false, setAsDefault: false }
+      };
+      return { qualityGate: newQualityGate };
+    });
+  };
+
+  render() {
+    const { organization, metrics, refreshQualityGates } = this.props;
+    const { qualityGate } = this.state;
+
+    if (!qualityGate) {
+      return null;
+    }
+
+    return (
+      <>
+        <Helmet title={qualityGate.name} />
+        <div className="layout-page-main">
+          <DetailsHeader
+            onSetDefault={this.handleSetDefault}
+            organization={organization}
+            qualityGate={qualityGate}
+            refreshItem={this.fetchDetails}
+            refreshList={refreshQualityGates}
+          />
+          <DetailsContent
+            isDefault={checkIfDefault(qualityGate, this.props.qualityGates)}
+            metrics={metrics}
+            onAddCondition={this.handleAddCondition}
+            onRemoveCondition={this.handleRemoveCondition}
+            onSaveCondition={this.handleSaveCondition}
+            organization={organization}
+            qualityGate={qualityGate}
+          />
+        </div>
+      </>
+    );
+  }
+}
+
+const mapDispatchToProps: DispatchToProps = { fetchMetrics };
+
+const mapStateToProps = (state: any): StateToProps => ({
+  metrics: getMetrics(state)
+});
+
+export default connect<StateToProps, DispatchToProps, OwnProps>(
+  mapStateToProps,
+  mapDispatchToProps
+)(DetailsApp);
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js
deleted file mode 100644 (file)
index 35b37f1..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import Conditions from './Conditions';
-import Projects from './Projects';
-import DocTooltip from '../../../components/docs/DocTooltip';
-import { translate } from '../../../helpers/l10n';
-
-export default class DetailsContent extends React.PureComponent {
-  render() {
-    const { gate, metrics, organization } = this.props;
-    const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props;
-    const conditions = gate.conditions || [];
-    const actions = gate.actions || {};
-
-    const defaultMessage = actions.associateProjects
-      ? translate('quality_gates.projects_for_default.edit')
-      : translate('quality_gates.projects_for_default');
-
-    return (
-      <div className="layout-page-main-inner">
-        <Conditions
-          qualityGate={gate}
-          conditions={conditions}
-          metrics={metrics}
-          edit={actions.manageConditions}
-          onAddCondition={onAddCondition}
-          onSaveCondition={onSaveCondition}
-          onDeleteCondition={onDeleteCondition}
-          organization={organization}
-        />
-
-        <div id="quality-gate-projects" className="quality-gate-section">
-          <header className="display-flex-center spacer-bottom">
-            <h3>{translate('quality_gates.projects')}</h3>
-            <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-projects" />
-          </header>
-          {gate.isDefault ? (
-            defaultMessage
-          ) : (
-            <Projects
-              qualityGate={gate}
-              edit={actions.associateProjects}
-              organization={organization}
-            />
-          )}
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
new file mode 100644 (file)
index 0000000..7463d77
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Conditions from './Conditions';
+import Projects from './Projects';
+import DocTooltip from '../../../components/docs/DocTooltip';
+import { translate } from '../../../helpers/l10n';
+import { Condition as ICondition, Metric, QualityGate } from '../../../app/types';
+
+interface Props {
+  isDefault?: boolean;
+  metrics: { [key: string]: Metric };
+  organization?: string;
+  onAddCondition: (metric: string) => void;
+  onRemoveCondition: (Condition: ICondition) => void;
+  onSaveCondition: (newCondition: ICondition, oldCondition: ICondition) => void;
+  qualityGate: QualityGate;
+}
+
+export default class DetailsContent extends React.PureComponent<Props> {
+  render() {
+    const { isDefault, metrics, organization, qualityGate } = this.props;
+    const conditions = qualityGate.conditions || [];
+    const actions = qualityGate.actions || ({} as any);
+
+    return (
+      <div className="layout-page-main-inner">
+        <Conditions
+          canEdit={actions.manageConditions}
+          conditions={conditions}
+          metrics={metrics}
+          onAddCondition={this.props.onAddCondition}
+          onRemoveCondition={this.props.onRemoveCondition}
+          onSaveCondition={this.props.onSaveCondition}
+          organization={organization}
+          qualityGate={qualityGate}
+        />
+
+        <div className="quality-gate-section" id="quality-gate-projects">
+          <header className="display-flex-center spacer-bottom">
+            <h3>{translate('quality_gates.projects')}</h3>
+            <DocTooltip className="spacer-left" doc="quality-gates/quality-gate-projects" />
+          </header>
+          {isDefault ? (
+            translate('quality_gates.projects_for_default')
+          ) : (
+            <Projects
+              canEdit={actions.associateProjects}
+              organization={organization}
+              qualityGate={qualityGate}
+            />
+          )}
+        </div>
+      </div>
+    );
+  }
+}
index c4c869924a3b4b3cd5449da4154546c221f383d6..b87ceed7ad2c07b15d3bcc1a01de561431f1fb42 100644 (file)
@@ -22,52 +22,39 @@ import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
 import RenameQualityGateForm from './RenameQualityGateForm';
 import CopyQualityGateForm from './CopyQualityGateForm';
 import DeleteQualityGateForm from './DeleteQualityGateForm';
-import { fetchQualityGate, QualityGate, setQualityGateAsDefault } from '../../../api/quality-gates';
+import { setQualityGateAsDefault } from '../../../api/quality-gates';
 import { Button } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
+import { QualityGate } from '../../../app/types';
 
 interface Props {
-  qualityGate: QualityGate;
-  onRename: (qualityGate: QualityGate, newName: string) => void;
-  onCopy: (newQualityGate: QualityGate) => void;
-  onSetAsDefault: (qualityGate: QualityGate) => void;
-  onDelete: (qualityGate: QualityGate) => void;
   organization?: string;
+  qualityGate: QualityGate;
+  onSetDefault: () => void;
+  refreshItem: () => Promise<void>;
+  refreshList: () => Promise<void>;
 }
 
-interface State {
-  openPopup?: string;
-}
-
-export default class DetailsHeader extends React.PureComponent<Props, State> {
-  state = { openPopup: undefined };
-
-  handleRenameClick = () => {
-    this.setState({ openPopup: 'rename' });
-  };
-
-  handleCopyClick = () => {
-    this.setState({ openPopup: 'copy' });
+export default class DetailsHeader extends React.PureComponent<Props> {
+  handleActionRefresh = () => {
+    const { refreshItem, refreshList } = this.props;
+    return Promise.all([refreshItem(), refreshList()]).then(() => {}, () => {});
   };
 
   handleSetAsDefaultClick = () => {
-    const { qualityGate, onSetAsDefault, organization } = this.props;
+    const { organization, qualityGate } = this.props;
     if (!qualityGate.isDefault) {
-      setQualityGateAsDefault({ id: qualityGate.id, organization })
-        .then(() => fetchQualityGate({ id: qualityGate.id, organization }))
-        .then(qualityGate => onSetAsDefault(qualityGate), () => {});
+      // Optimistic update
+      this.props.onSetDefault();
+      setQualityGateAsDefault({ id: qualityGate.id, organization }).then(
+        this.handleActionRefresh,
+        this.handleActionRefresh
+      );
     }
   };
 
-  handleDeleteClick = () => {
-    this.setState({ openPopup: 'delete' });
-  };
-
-  handleClosePopup = () => this.setState({ openPopup: undefined });
-
   render() {
     const { organization, qualityGate } = this.props;
-    const { openPopup } = this.state;
     const actions = qualityGate.actions || ({} as any);
     return (
       <div className="layout-page-header-panel layout-page-main-header issues-main-header">
@@ -80,17 +67,18 @@ export default class DetailsHeader extends React.PureComponent<Props, State> {
 
             <div className="pull-right">
               {actions.rename && (
-                <Button id="quality-gate-rename" onClick={this.handleRenameClick}>
-                  {translate('rename')}
-                </Button>
+                <RenameQualityGateForm
+                  onRename={this.handleActionRefresh}
+                  organization={organization}
+                  qualityGate={qualityGate}
+                />
               )}
               {actions.copy && (
-                <Button
-                  className="little-spacer-left"
-                  id="quality-gate-copy"
-                  onClick={this.handleCopyClick}>
-                  {translate('copy')}
-                </Button>
+                <CopyQualityGateForm
+                  onCopy={this.handleActionRefresh}
+                  organization={organization}
+                  qualityGate={qualityGate}
+                />
               )}
               {actions.setAsDefault && (
                 <Button
@@ -101,35 +89,8 @@ export default class DetailsHeader extends React.PureComponent<Props, State> {
                 </Button>
               )}
               {actions.delete && (
-                <Button
-                  className="little-spacer-left button-red"
-                  id="quality-gate-delete"
-                  onClick={this.handleDeleteClick}>
-                  {translate('delete')}
-                </Button>
-              )}
-              {openPopup === 'rename' && (
-                <RenameQualityGateForm
-                  onClose={this.handleClosePopup}
-                  onRename={this.props.onRename}
-                  organization={organization}
-                  qualityGate={qualityGate}
-                />
-              )}
-
-              {openPopup === 'copy' && (
-                <CopyQualityGateForm
-                  onClose={this.handleClosePopup}
-                  onCopy={this.props.onCopy}
-                  organization={organization}
-                  qualityGate={qualityGate}
-                />
-              )}
-
-              {openPopup === 'delete' && (
                 <DeleteQualityGateForm
-                  onClose={this.handleClosePopup}
-                  onDelete={this.props.onDelete}
+                  onDelete={this.handleActionRefresh}
                   organization={organization}
                   qualityGate={qualityGate}
                 />
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.js
deleted file mode 100644 (file)
index 37fb224..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { translate } from '../../../helpers/l10n';
-
-export default function Intro() {
-  return (
-    <div className="layout-page-main">
-      <div className="layout-page-main-inner">
-        <div className="search-navigator-intro markdown">
-          <p>{translate('quality_gates.intro.1')}</p>
-          <p>{translate('quality_gates.intro.2')}</p>
-        </div>
-      </div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.tsx
new file mode 100644 (file)
index 0000000..ea5edc9
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default function Intro() {
+  return (
+    <div className="layout-page-main">
+      <div className="layout-page-main-inner">
+        <div className="search-navigator-intro markdown">
+          <p>{translate('quality_gates.intro.1')}</p>
+          <p>{translate('quality_gates.intro.2')}</p>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.js b/server/sonar-web/src/main/js/apps/quality-gates/components/List.js
deleted file mode 100644 (file)
index a1319df..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { Link } from 'react-router';
-import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
-import { translate } from '../../../helpers/l10n';
-import { getQualityGateUrl } from '../../../helpers/urls';
-
-export default function List({ organization, qualityGates }) {
-  return (
-    <div className="list-group">
-      {qualityGates.map(qualityGate => (
-        <Link
-          activeClassName="active"
-          className="list-group-item"
-          data-id={qualityGate.id}
-          key={qualityGate.id}
-          to={getQualityGateUrl(String(qualityGate.id), organization && organization.key)}>
-          <table>
-            <tbody>
-              <tr>
-                <td>{qualityGate.name}</td>
-                <td className="thin nowrap spacer-left text-right">
-                  {qualityGate.isDefault && <span className="badge">{translate('default')}</span>}
-                  {qualityGate.isBuiltIn && (
-                    <BuiltInQualityGateBadge className="little-spacer-left" />
-                  )}
-                </td>
-              </tr>
-            </tbody>
-          </table>
-        </Link>
-      ))}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
new file mode 100644 (file)
index 0000000..5586c52
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
+import { translate } from '../../../helpers/l10n';
+import { getQualityGateUrl } from '../../../helpers/urls';
+import { QualityGate } from '../../../app/types';
+
+interface Props {
+  organization?: string;
+  qualityGates: QualityGate[];
+}
+
+export default function List({ organization, qualityGates }: Props) {
+  return (
+    <div className="list-group">
+      {qualityGates.map(qualityGate => (
+        <Link
+          activeClassName="active"
+          className="list-group-item"
+          data-id={qualityGate.id}
+          key={qualityGate.id}
+          to={getQualityGateUrl(String(qualityGate.id), organization)}>
+          <table>
+            <tbody>
+              <tr>
+                <td>{qualityGate.name}</td>
+                <td className="thin nowrap spacer-left text-right">
+                  {qualityGate.isDefault && <span className="badge">{translate('default')}</span>}
+                  {qualityGate.isBuiltIn && (
+                    <BuiltInQualityGateBadge className="little-spacer-left" />
+                  )}
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </Link>
+      ))}
+    </div>
+  );
+}
index da61b1c5623ceece9079063f61f74ab7f3eb3cd4..7ff64a81d9e276f72c8bb9caadd94aef763bd369 100644 (file)
  */
 import * as React from 'react';
 import CreateQualityGateForm from '../components/CreateQualityGateForm';
-import { QualityGate } from '../../../api/quality-gates';
-import { Organization } from '../../../app/types';
 import DocTooltip from '../../../components/docs/DocTooltip';
-import { Button } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
   canCreate: boolean;
-  onAdd: (qualityGate: QualityGate) => void;
-  organization?: Organization;
+  refreshQualityGates: () => Promise<void>;
+  organization?: string;
 }
 
-interface State {
-  createQualityGateOpen: boolean;
-}
-
-export default class ListHeader extends React.PureComponent<Props, State> {
-  state = { createQualityGateOpen: false };
-
-  openCreateQualityGateForm = () => {
-    this.setState({ createQualityGateOpen: true });
-  };
-
-  closeCreateQualityGateForm = () => {
-    this.setState({ createQualityGateOpen: false });
-  };
-
-  render() {
-    const { organization } = this.props;
-
-    return (
-      <header className="page-header">
-        {this.props.canCreate && (
-          <div className="page-actions">
-            <Button id="quality-gate-add" onClick={this.openCreateQualityGateForm}>
-              {translate('create')}
-            </Button>
-          </div>
-        )}
-        <div className="display-flex-center">
-          <h1 className="page-title">{translate('quality_gates.page')}</h1>
-          <DocTooltip className="spacer-left" doc="quality-gates/quality-gate" />
+export default function ListHeader({ canCreate, refreshQualityGates, organization }: Props) {
+  return (
+    <header className="page-header">
+      {canCreate && (
+        <div className="page-actions">
+          <CreateQualityGateForm onCreate={refreshQualityGates} organization={organization} />
         </div>
-        {this.state.createQualityGateOpen && (
-          <CreateQualityGateForm
-            onClose={this.closeCreateQualityGateForm}
-            onCreate={this.props.onAdd}
-            organization={organization && organization.key}
-          />
-        )}
-      </header>
-    );
-  }
+      )}
+
+      <div className="display-flex-center">
+        <h1 className="page-title">{translate('quality_gates.page')}</h1>
+        <DocTooltip className="spacer-left" doc="quality-gates/quality-gate" />
+      </div>
+    </header>
+  );
 }
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js
deleted file mode 100644 (file)
index b51bed9..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { find, without } from 'lodash';
-import SelectList, { Filter } from '../../../components/SelectList/SelectList';
-import { translate } from '../../../helpers/l10n';
-import {
-  searchGates,
-  associateGateWithProject,
-  dissociateGateWithProject
-} from '../../../api/quality-gates';
-/*:: import { Project } from '../../projects/types'; */
-
-/*::
-type State = {
-  projects: Projects[],
-  selectedProjects: string[]
-};
-*/
-
-export default class Projects extends React.PureComponent {
-  state /*: State */ = { projects: [], selectedProjects: [] };
-
-  componentDidMount() {
-    this.handleSearch('', Filter.Selected);
-  }
-
-  handleSearch = (query /*: string*/, selected /*: string */) => {
-    return searchGates({
-      gateId: this.props.qualityGate.id,
-      organization: this.props.organization,
-      pageSize: 100,
-      query: query !== '' ? query : undefined,
-      selected
-    }).then(data => {
-      this.setState({
-        projects: data.results,
-        selectedProjects: data.results
-          .filter(project => project.selected)
-          .map(project => project.id)
-      });
-    });
-  };
-
-  handleSelect = (id /*: string*/) => {
-    return associateGateWithProject({
-      gateId: this.props.qualityGate.id,
-      organization: this.props.organization,
-      projectId: id
-    }).then(() => {
-      this.setState((state /*: State*/) => ({
-        selectedProjects: [...state.selectedProjects, id]
-      }));
-    });
-  };
-
-  handleUnselect = (id /*: string*/) => {
-    return dissociateGateWithProject({
-      gateId: this.props.qualityGate.id,
-      organization: this.props.organization,
-      projectId: id
-    }).then(
-      () => {
-        this.setState((state /*: State*/) => ({
-          selectedProjects: without(state.selectedProjects, id)
-        }));
-      },
-      () => {}
-    );
-  };
-
-  renderElement = (id /*: string*/) /*: React.ReactNode*/ => {
-    const project = find(this.state.projects, { id });
-    return project === undefined ? id : project.name;
-  };
-
-  render() {
-    return (
-      <SelectList
-        elements={this.state.projects.map(project => project.id)}
-        labelAll={translate('quality_gates.projects.all')}
-        labelSelected={translate('quality_gates.projects.with')}
-        labelUnselected={translate('quality_gates.projects.without')}
-        onSearch={this.handleSearch}
-        onSelect={this.handleSelect}
-        onUnselect={this.handleUnselect}
-        renderElement={this.renderElement}
-        selectedElements={this.state.selectedProjects}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx
new file mode 100644 (file)
index 0000000..8153ec3
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { find, without } from 'lodash';
+import SelectList, { Filter } from '../../../components/SelectList/SelectList';
+import { translate } from '../../../helpers/l10n';
+import {
+  searchGates,
+  associateGateWithProject,
+  dissociateGateWithProject
+} from '../../../api/quality-gates';
+import { QualityGate } from '../../../app/types';
+
+interface Props {
+  canEdit?: boolean;
+  organization?: string;
+  qualityGate: QualityGate;
+}
+
+interface State {
+  projects: Array<{ id: string; name: string; selected: boolean }>;
+  selectedProjects: string[];
+}
+
+export default class Projects extends React.PureComponent<Props, State> {
+  state: State = { projects: [], selectedProjects: [] };
+
+  componentDidMount() {
+    this.handleSearch('', Filter.Selected);
+  }
+
+  handleSearch = (query: string, selected: string) => {
+    return searchGates({
+      gateId: this.props.qualityGate.id,
+      organization: this.props.organization,
+      pageSize: 100,
+      query: query !== '' ? query : undefined,
+      selected
+    }).then(data => {
+      this.setState({
+        projects: data.results,
+        selectedProjects: data.results
+          .filter(project => project.selected)
+          .map(project => project.id)
+      });
+    });
+  };
+
+  handleSelect = (id: string) => {
+    return associateGateWithProject({
+      gateId: this.props.qualityGate.id,
+      organization: this.props.organization,
+      projectId: id
+    }).then(() => {
+      this.setState(state => ({
+        selectedProjects: [...state.selectedProjects, id]
+      }));
+    });
+  };
+
+  handleUnselect = (id: string) => {
+    return dissociateGateWithProject({
+      gateId: this.props.qualityGate.id,
+      organization: this.props.organization,
+      projectId: id
+    }).then(
+      () => {
+        this.setState(state => ({
+          selectedProjects: without(state.selectedProjects, id)
+        }));
+      },
+      () => {}
+    );
+  };
+
+  renderElement = (id: string): React.ReactNode => {
+    const project = find(this.state.projects, { id });
+    return project === undefined ? id : project.name;
+  };
+
+  render() {
+    return (
+      <SelectList
+        elements={this.state.projects.map(project => project.id)}
+        labelAll={translate('quality_gates.projects.all')}
+        labelSelected={translate('quality_gates.projects.with')}
+        labelUnselected={translate('quality_gates.projects.without')}
+        onSearch={this.handleSearch}
+        onSelect={this.handleSelect}
+        onUnselect={this.handleUnselect}
+        readOnly={!this.props.canEdit}
+        renderElement={this.renderElement}
+        selectedElements={this.state.selectedProjects}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
deleted file mode 100644 (file)
index dbd4a44..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import Helmet from 'react-helmet';
-import ListHeader from './ListHeader';
-import List from './List';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import { fetchQualityGates } from '../../../api/quality-gates';
-import { translate } from '../../../helpers/l10n';
-import { getQualityGateUrl } from '../../../helpers/urls';
-import '../styles.css';
-
-export default class QualityGatesApp extends Component {
-  static contextTypes = {
-    router: PropTypes.object.isRequired
-  };
-
-  state = {};
-
-  componentDidMount() {
-    this.fetchQualityGates();
-    // $FlowFixMe
-    document.body.classList.add('white-page');
-    // $FlowFixMe
-    document.documentElement.classList.add('white-page');
-    const footer = document.getElementById('footer');
-    if (footer) {
-      footer.classList.add('page-footer-with-sidebar');
-    }
-  }
-
-  componentWillUnmount() {
-    // $FlowFixMe
-    document.body.classList.remove('white-page');
-    // $FlowFixMe
-    document.documentElement.classList.remove('white-page');
-    const footer = document.getElementById('footer');
-    if (footer) {
-      footer.classList.remove('page-footer-with-sidebar');
-    }
-  }
-
-  fetchQualityGates = () =>
-    fetchQualityGates({
-      organization: this.props.organization && this.props.organization.key
-    }).then(
-      ({ actions, qualitygates: qualityGates }) => {
-        const { organization, updateStore } = this.props;
-        updateStore({ actions, qualityGates });
-        if (qualityGates && qualityGates.length === 1 && !actions.create) {
-          this.context.router.replace(
-            getQualityGateUrl(String(qualityGates[0].id), organization && organization.key)
-          );
-        }
-      },
-      () => {}
-    );
-
-  render() {
-    const { children, qualityGates, actions, organization } = this.props;
-    const defaultTitle = translate('quality_gates.page');
-    return (
-      <div id="quality-gates-page" className="layout-page">
-        <Suggestions suggestions="quality_gates" />
-        <Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
-
-        <ScreenPositionHelper className="layout-page-side-outer">
-          {({ top }) => (
-            <div className="layout-page-side" style={{ top }}>
-              <div className="layout-page-side-inner">
-                <div className="layout-page-filters">
-                  <ListHeader
-                    canCreate={actions && actions.create}
-                    onAdd={this.props.addQualityGate}
-                    organization={organization}
-                  />
-                  {qualityGates && <List organization={organization} qualityGates={qualityGates} />}
-                </div>
-              </div>
-            </div>
-          )}
-        </ScreenPositionHelper>
-        {qualityGates != null &&
-          React.Children.map(children, child => React.cloneElement(child, { organization }))}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx
new file mode 100644 (file)
index 0000000..70d7b20
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import * as PropTypes from 'prop-types';
+import Helmet from 'react-helmet';
+import ListHeader from './ListHeader';
+import List from './List';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import { fetchQualityGates } from '../../../api/quality-gates';
+import { translate } from '../../../helpers/l10n';
+import { getQualityGateUrl } from '../../../helpers/urls';
+import { Organization, QualityGate } from '../../../app/types';
+import '../styles.css';
+
+interface Props {
+  children: React.ReactElement<{
+    organization?: string;
+    refreshQualityGates: () => Promise<void>;
+  }>;
+  organization: Pick<Organization, 'key'>;
+}
+
+interface State {
+  canCreate: boolean;
+  loading: boolean;
+  qualityGates: QualityGate[];
+}
+
+export default class QualityGatesApp extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  static contextTypes = {
+    router: PropTypes.object.isRequired
+  };
+
+  state: State = { canCreate: false, loading: true, qualityGates: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchQualityGates();
+
+    document.body.classList.add('white-page');
+    document.documentElement.classList.add('white-page');
+    const footer = document.getElementById('footer');
+    if (footer) {
+      footer.classList.add('page-footer-with-sidebar');
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    document.body.classList.remove('white-page');
+    document.documentElement.classList.remove('white-page');
+    const footer = document.getElementById('footer');
+    if (footer) {
+      footer.classList.remove('page-footer-with-sidebar');
+    }
+  }
+
+  fetchQualityGates = () => {
+    const { organization } = this.props;
+    return fetchQualityGates({ organization: organization && organization.key }).then(
+      ({ actions, qualitygates: qualityGates }) => {
+        if (this.mounted) {
+          this.setState({ canCreate: actions.create, loading: false, qualityGates });
+
+          if (qualityGates && qualityGates.length === 1 && !actions.create) {
+            this.context.router.replace(
+              getQualityGateUrl(String(qualityGates[0].id), organization && organization.key)
+            );
+          }
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleSetDefault = (qualityGate: QualityGate) => {
+    this.setState(({ qualityGates }) => {
+      return {
+        qualityGates: qualityGates.map(candidate => {
+          if (candidate.isDefault || candidate.id === qualityGate.id) {
+            return { ...candidate, isDefault: candidate.id === qualityGate.id };
+          }
+          return candidate;
+        })
+      };
+    });
+  };
+
+  render() {
+    const { children } = this.props;
+    const { canCreate, loading, qualityGates } = this.state;
+    const defaultTitle = translate('quality_gates.page');
+    const organization = this.props.organization && this.props.organization.key;
+
+    return (
+      <>
+        <Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
+        <div className="layout-page" id="quality-gates-page">
+          <Suggestions suggestions="quality_gates" />
+
+          <ScreenPositionHelper className="layout-page-side-outer">
+            {({ top }) => (
+              <div className="layout-page-side" style={{ top }}>
+                <div className="layout-page-side-inner">
+                  <div className="layout-page-filters">
+                    <ListHeader
+                      canCreate={canCreate}
+                      organization={organization}
+                      refreshQualityGates={this.fetchQualityGates}
+                    />
+                    {qualityGates.length > 0 && (
+                      <List organization={organization} qualityGates={qualityGates} />
+                    )}
+                  </div>
+                </div>
+              </div>
+            )}
+          </ScreenPositionHelper>
+          {!loading &&
+            React.cloneElement(children, {
+              onSetDefault: this.handleSetDefault,
+              organization,
+              qualityGates,
+              refreshQualityGates: this.fetchQualityGates
+            })}
+        </div>
+      </>
+    );
+  }
+}
index aebd61dda7337e660ee86d05607c335e30a25ba9..5845c2c7cab3a3cdd5c4c90b8d02d9868b3477e8 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { QualityGate, renameQualityGate } from '../../../api/quality-gates';
-import Modal from '../../../components/controls/Modal';
-import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { renameQualityGate } from '../../../api/quality-gates';
+import { Button } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
+import { QualityGate } from '../../../app/types';
 
 interface Props {
-  qualityGate: QualityGate;
-  onRename: (qualityGate: QualityGate, newName: string) => void;
-  onClose: () => void;
+  onRename: () => Promise<void>;
   organization?: string;
+  qualityGate: QualityGate;
 }
 
 interface State {
-  loading: boolean;
   name: string;
 }
 
 export default class RenameQualityGateForm extends React.PureComponent<Props, State> {
-  mounted = false;
-
   constructor(props: Props) {
     super(props);
-    this.state = { loading: false, name: props.qualityGate.name };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
+    this.state = { name: props.qualityGate.name };
   }
 
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+  handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     this.setState({ name: event.currentTarget.value });
   };
 
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
+  onRename = () => {
     const { qualityGate, organization } = this.props;
     const { name } = this.state;
-    if (name) {
-      this.setState({ loading: true });
-      renameQualityGate({ id: qualityGate.id, name, organization }).then(
-        () => {
-          this.props.onRename(qualityGate, name);
-          this.props.onClose();
-        },
-        () => {
-          if (this.mounted) {
-            this.setState({ loading: false });
-          }
-        }
-      );
+
+    if (!name) {
+      return undefined;
     }
+
+    return renameQualityGate({ id: qualityGate.id, name, organization }).then(() =>
+      this.props.onRename()
+    );
   };
 
   render() {
     const { qualityGate } = this.props;
-    const { loading, name } = this.state;
-    const header = translate('quality_gates.rename');
-    const submitDisabled = loading || !name || (qualityGate && qualityGate.name === name);
+    const { name } = this.state;
+    const confirmDisable = !name || (qualityGate && qualityGate.name === name);
 
     return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <form id="quality-gate-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
+      <ConfirmButton
+        confirmButtonText={translate('rename')}
+        confirmDisable={confirmDisable}
+        modalBody={
+          <div className="modal-field">
+            <label htmlFor="quality-gate-form-name">
+              {translate('name')}
+              <em className="mandatory">*</em>
+            </label>
+            <input
+              autoFocus={true}
+              id="quality-gate-form-name"
+              maxLength={100}
+              onChange={this.handleNameChange}
+              required={true}
+              size={50}
+              type="text"
+              value={name}
+            />
           </div>
-          <div className="modal-body">
-            <div className="modal-field">
-              <label htmlFor="quality-gate-form-name">
-                {translate('name')}
-                <em className="mandatory">*</em>
-              </label>
-              <input
-                autoFocus={true}
-                id="quality-gate-form-name"
-                maxLength={100}
-                onChange={this.handleNameChange}
-                required={true}
-                size={50}
-                type="text"
-                value={name}
-              />
-            </div>
-          </div>
-          <div className="modal-foot">
-            {loading && <i className="spinner spacer-right" />}
-            <SubmitButton className="js-confirm" disabled={submitDisabled}>
-              {translate('rename')}
-            </SubmitButton>
-            <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
+        }
+        modalHeader={translate('quality_gates.rename')}
+        onConfirm={this.onRename}>
+        {({ onClick }) => (
+          <Button id="quality-gate-rename" onClick={onClick}>
+            {translate('rename')}
+          </Button>
+        )}
+      </ConfirmButton>
     );
   }
 }
index 2dda968c3e0b3ad483a70f89d97891547d288d4c..f5c328f348a0972755fa1e6710eceea8de4755ed 100644 (file)
@@ -74,12 +74,12 @@ export default class ThresholdInput extends React.PureComponent<Props> {
 
     return (
       <input
-        name={name}
-        type="text"
         className="input-tiny text-middle"
-        value={value}
         data-type={metric.type}
+        name={name}
         onChange={this.handleChange}
+        type="text"
+        value={value}
       />
     );
   }
index e52447250feecfefedc46e8218e87b50f6bf13d2..8b8f0780d5540df2ddacf544f54711682f8adcc7 100644 (file)
@@ -26,7 +26,7 @@ describe('on strings', () => {
   const metric = { id: '1', key: 'foo', name: 'Foo', type: 'INTEGER' };
   it('should render text input', () => {
     const input = shallow(
-      <ThresholdInput name="foo" value="2" metric={metric} onChange={jest.fn()} />
+      <ThresholdInput metric={metric} name="foo" onChange={jest.fn()} value="2" />
     ).find('input');
     expect(input.length).toEqual(1);
     expect(input.prop('name')).toEqual('foo');
@@ -36,7 +36,7 @@ describe('on strings', () => {
   it('should change', () => {
     const onChange = jest.fn();
     const input = shallow(
-      <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} />
+      <ThresholdInput metric={metric} name="foo" onChange={onChange} value="2" />
     ).find('input');
     change(input, 'bar');
     expect(onChange).toBeCalledWith('bar');
@@ -47,7 +47,7 @@ describe('on ratings', () => {
   const metric = { id: '1', key: 'foo', name: 'Foo', type: 'RATING' };
   it('should render Select', () => {
     const select = shallow(
-      <ThresholdInput name="foo" value="2" metric={metric} onChange={jest.fn()} />
+      <ThresholdInput metric={metric} name="foo" onChange={jest.fn()} value="2" />
     ).find('Select');
     expect(select.length).toEqual(1);
     expect(select.prop('value')).toEqual('2');
@@ -56,7 +56,7 @@ describe('on ratings', () => {
   it('should set', () => {
     const onChange = jest.fn();
     const select = shallow(
-      <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} />
+      <ThresholdInput metric={metric} name="foo" onChange={onChange} value="2" />
     ).find('Select');
     (select.prop('onChange') as Function)({ label: 'D', value: '4' });
     expect(onChange).toBeCalledWith('4');
@@ -65,7 +65,7 @@ describe('on ratings', () => {
   it('should unset', () => {
     const onChange = jest.fn();
     const select = shallow(
-      <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} />
+      <ThresholdInput metric={metric} name="foo" onChange={onChange} value="2" />
     ).find('Select');
     (select.prop('onChange') as Function)(null);
     expect(onChange).toBeCalledWith('');
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js b/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js
deleted file mode 100644 (file)
index 224264d..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { connect } from 'react-redux';
-import {
-  deleteQualityGate,
-  showQualityGate,
-  renameQualityGate,
-  copyQualityGate,
-  setQualityGateAsDefault,
-  addCondition,
-  deleteCondition,
-  saveCondition
-} from '../store/actions';
-import Details from '../components/Details';
-import { getMetrics, getQualityGatesAppState } from '../../../store/rootReducer';
-import { fetchMetrics } from '../../../store/rootActions';
-
-const mapStateToProps = state => ({
-  ...getQualityGatesAppState(state),
-  metrics: getMetrics(state)
-});
-
-const mapDispatchToProps = dispatch => ({
-  onShow: qualityGate => dispatch(showQualityGate(qualityGate)),
-  onDelete: qualityGate => dispatch(deleteQualityGate(qualityGate)),
-  onRename: (qualityGate, newName) => dispatch(renameQualityGate(qualityGate, newName)),
-  onCopy: qualityGate => dispatch(copyQualityGate(qualityGate)),
-  onSetAsDefault: qualityGate => dispatch(setQualityGateAsDefault(qualityGate)),
-  onAddCondition: metric => dispatch(addCondition(metric)),
-  onSaveCondition: (oldCondition, newCondition) =>
-    dispatch(saveCondition(oldCondition, newCondition)),
-  onDeleteCondition: condition => dispatch(deleteCondition(condition)),
-  fetchMetrics: () => dispatch(fetchMetrics())
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(Details);
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/containers/QualityGatesAppContainer.js b/server/sonar-web/src/main/js/apps/quality-gates/containers/QualityGatesAppContainer.js
deleted file mode 100644 (file)
index 0526742..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { connect } from 'react-redux';
-import { setState, addQualityGate, deleteQualityGate } from '../store/actions';
-import QualityGateApp from '../components/QualityGatesApp';
-import { getQualityGatesAppState } from '../../../store/rootReducer';
-
-const mapStateToProps = state => getQualityGatesAppState(state);
-
-const mapDispatchToProps = dispatch => ({
-  updateStore: nextState => dispatch(setState(nextState)),
-  addQualityGate: qualityGate => dispatch(addQualityGate(qualityGate)),
-  deleteQualityGate: qualityGate => dispatch(deleteQualityGate(qualityGate))
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(QualityGateApp);
index c173e2762b5c8bc6056107ac0fccd22eb6ab44f2..4d9769f5a709db88f8ea2f41bf7c99d44cd67904 100644 (file)
@@ -22,7 +22,7 @@ import { RouterState, IndexRouteProps, RouteComponent } from 'react-router';
 const routes = [
   {
     getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) {
-      import('./containers/QualityGatesAppContainer').then(i => callback(null, i.default));
+      import('./components/QualityGatesApp').then(i => callback(null, i.default));
     },
     childRoutes: [
       {
@@ -33,7 +33,7 @@ const routes = [
       {
         path: 'show/:id',
         getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) {
-          import('./containers/DetailsContainer').then(i => callback(null, i.default));
+          import('./components/DetailsApp').then(i => callback(null, i.default));
         }
       }
     ]
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/store/actions.js b/server/sonar-web/src/main/js/apps/quality-gates/store/actions.js
deleted file mode 100644 (file)
index 17b9580..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-export const SET_STATE = 'qualityGates/SET_STATE';
-export function setState(nextState) {
-  return {
-    type: SET_STATE,
-    nextState
-  };
-}
-
-export const ADD = 'qualityGates/ADD';
-export function addQualityGate(qualityGate) {
-  return {
-    type: ADD,
-    qualityGate
-  };
-}
-
-export const DELETE = 'qualityGates/DELETE';
-export function deleteQualityGate(qualityGate) {
-  return {
-    type: DELETE,
-    qualityGate
-  };
-}
-
-export const SHOW = 'qualityGates/SHOW';
-export function showQualityGate(qualityGate) {
-  return {
-    type: SHOW,
-    qualityGate
-  };
-}
-
-export const RENAME = 'qualityGates/RENAME';
-export function renameQualityGate(qualityGate, newName) {
-  return {
-    type: RENAME,
-    qualityGate,
-    newName
-  };
-}
-
-export const COPY = 'qualityGates/COPY';
-export function copyQualityGate(qualityGate) {
-  return {
-    type: COPY,
-    qualityGate
-  };
-}
-
-export const SET_AS_DEFAULT = 'qualityGates/SET_AS_DEFAULT';
-export function setQualityGateAsDefault(qualityGate) {
-  return {
-    type: SET_AS_DEFAULT,
-    qualityGate
-  };
-}
-
-export const ADD_CONDITION = 'qualityGates/ADD_CONDITION';
-export function addCondition(metric) {
-  return {
-    type: ADD_CONDITION,
-    metric
-  };
-}
-
-export const SAVE_CONDITION = 'qualityGates/SAVE_CONDITION';
-export function saveCondition(oldCondition, newCondition) {
-  return {
-    type: SAVE_CONDITION,
-    oldCondition,
-    newCondition
-  };
-}
-
-export const DELETE_CONDITION = 'qualityGates/DELETE_CONDITION';
-export function deleteCondition(condition) {
-  return {
-    type: DELETE_CONDITION,
-    condition
-  };
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/store/rootReducer.js b/server/sonar-web/src/main/js/apps/quality-gates/store/rootReducer.js
deleted file mode 100644 (file)
index c9c4814..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 {
-  SET_STATE,
-  ADD,
-  DELETE,
-  SHOW,
-  RENAME,
-  COPY,
-  SET_AS_DEFAULT,
-  ADD_CONDITION,
-  DELETE_CONDITION,
-  SAVE_CONDITION
-} from './actions';
-import { checkIfDefault, addCondition, deleteCondition, replaceCondition } from './utils';
-
-const initialState = {};
-
-export default function rootReducer(state = initialState, action = {}) {
-  switch (action.type) {
-    case SET_STATE:
-      return { ...state, ...action.nextState };
-    case ADD:
-    case COPY:
-      return { ...state, qualityGates: [...state.qualityGates, action.qualityGate] };
-    case DELETE:
-      return {
-        ...state,
-        qualityGates: state.qualityGates.filter(
-          candidate => candidate.id !== action.qualityGate.id
-        ),
-        qualityGate: state.qualityGate.id === action.qualityGate.id ? undefined : state.qualityGate
-      };
-    case SHOW:
-      return {
-        ...state,
-        qualityGate: {
-          ...action.qualityGate,
-          isDefault: checkIfDefault(action.qualityGate, state.qualityGates)
-        }
-      };
-    case RENAME:
-      return {
-        ...state,
-        qualityGates: state.qualityGates.map(candidate => {
-          return candidate.id === action.qualityGate.id
-            ? { ...candidate, name: action.newName }
-            : candidate;
-        }),
-        qualityGate: { ...state.qualityGate, name: action.newName }
-      };
-    case SET_AS_DEFAULT:
-      return {
-        ...state,
-        qualityGates: state.qualityGates.map(candidate => {
-          return { ...candidate, isDefault: candidate.id === action.qualityGate.id };
-        }),
-        qualityGate: {
-          ...action.qualityGate,
-          isDefault: state.qualityGate.id === action.qualityGate.id
-        }
-      };
-    case ADD_CONDITION:
-      return {
-        ...state,
-        qualityGate: addCondition(state.qualityGate, action.metric)
-      };
-    case DELETE_CONDITION:
-      return {
-        ...state,
-        qualityGate: deleteCondition(state.qualityGate, action.condition)
-      };
-    case SAVE_CONDITION:
-      return {
-        ...state,
-        qualityGate: replaceCondition(state.qualityGate, action.oldCondition, action.newCondition)
-      };
-    default:
-      return state;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/store/utils.js b/server/sonar-web/src/main/js/apps/quality-gates/store/utils.js
deleted file mode 100644 (file)
index 255c436..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-export function checkIfDefault(qualityGate, list) {
-  const finding = list.find(candidate => candidate.id === qualityGate.id);
-
-  return finding ? finding.isDefault : false;
-}
-
-export function addCondition(qualityGate, metric) {
-  const condition = {
-    metric,
-    op: 'LT',
-    warning: '',
-    error: ''
-  };
-  const oldConditions = qualityGate.conditions || [];
-  const conditions = [...oldConditions, condition];
-
-  return { ...qualityGate, conditions };
-}
-
-export function deleteCondition(qualityGate, condition) {
-  const conditions = qualityGate.conditions.filter(candidate => candidate !== condition);
-
-  return { ...qualityGate, conditions };
-}
-
-export function replaceCondition(qualityGate, oldCondition, newCondition) {
-  const conditions = qualityGate.conditions.map(candidate => {
-    return candidate === oldCondition ? newCondition : candidate;
-  });
-  return { ...qualityGate, conditions };
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts
new file mode 100644 (file)
index 0000000..845d6da
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { QualityGate, Condition } from '../../app/types';
+
+export function checkIfDefault(qualityGate: QualityGate, list: QualityGate[]): boolean {
+  const finding = list.find(candidate => candidate.id === qualityGate.id);
+  return (finding && finding.isDefault) || false;
+}
+
+export function addCondition(qualityGate: QualityGate, metric: string): QualityGate {
+  const condition: Condition = {
+    metric,
+    op: 'LT',
+    warning: '',
+    error: ''
+  };
+  const oldConditions = qualityGate.conditions || [];
+  const conditions = [...oldConditions, condition];
+  return { ...qualityGate, conditions };
+}
+
+export function deleteCondition(qualityGate: QualityGate, condition: Condition): QualityGate {
+  const conditions =
+    qualityGate.conditions && qualityGate.conditions.filter(candidate => candidate !== condition);
+  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;
+    });
+  return { ...qualityGate, conditions };
+}
index 1d893f211b30a15c1f072221f51b6932300b429c..8fa07588bb21097b753644d29438209769b6c483 100644 (file)
@@ -61,7 +61,7 @@ export default class SelectListListElement extends React.PureComponent<Props, St
 
   render() {
     return (
-      <li>
+      <li className={classNames({ 'select-list-list-disabled': this.props.disabled })}>
         <Checkbox
           checked={this.props.selected}
           className={classNames('select-list-list-checkbox', { active: this.props.active })}
index 38351d99ec16f8598cb7604efffae32764b0567f..17bbd55a5cf5a49772fa4c8b319df1f0abb72938 100644 (file)
@@ -1,7 +1,9 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should display a loader when checking 1`] = `
-<li>
+<li
+  className=""
+>
   <Checkbox
     checked={false}
     className="select-list-list-checkbox"
@@ -18,7 +20,9 @@ exports[`should display a loader when checking 1`] = `
 `;
 
 exports[`should display a loader when checking 2`] = `
-<li>
+<li
+  className=""
+>
   <Checkbox
     checked={false}
     className="select-list-list-checkbox"
index beb5c99211966125264cab7fd1ef9f74c6a4a1a5..6003cb7ad35445ae442b497ce2b4cb67fe12d342 100644 (file)
   margin-right: 10px;
 }
 
+.select-list-list-disabled {
+  cursor: not-allowed;
+}
+
+.select-list-list-disabled > a {
+  pointer-events: none;
+}
+
 .select-list-list-item {
   display: inline-block;
   vertical-align: middle;
index 354e11add9ca051d8e592a12cffa5df68e730eb2..6d6f558bdcb23ca897e5e694d7e406140eb2893d 100644 (file)
@@ -32,6 +32,7 @@ interface Props {
   children: (props: ChildrenProps) => React.ReactNode;
   confirmButtonText: string;
   confirmData?: string;
+  confirmDisable?: boolean;
   isDestructive?: boolean;
   modalBody: React.ReactNode;
   modalHeader: string;
@@ -82,7 +83,7 @@ export default class ConfirmButton extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { confirmButtonText, isDestructive, modalBody, modalHeader } = this.props;
+    const { confirmButtonText, confirmDisable, isDestructive, modalBody, modalHeader } = this.props;
 
     return (
       <>
@@ -107,7 +108,7 @@ export default class ConfirmButton extends React.PureComponent<Props, State> {
                   <DeferredSpinner className="spacer-right" loading={submitting} />
                   <SubmitButton
                     className={isDestructive ? 'button-red' : undefined}
-                    disabled={submitting}>
+                    disabled={submitting || confirmDisable}>
                     {confirmButtonText}
                   </SubmitButton>
                   <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
index 51ea1ad9971e8f68d7e4740485dd80aa4a0fe0ee..15c9d1ac30b877f215885eceec25efa51d3af993 100644 (file)
@@ -30,7 +30,6 @@ import organizationsMembers, * as fromOrganizationsMembers from './organizations
 import globalMessages, * as fromGlobalMessages from './globalMessages/duck';
 import permissionsApp, * as fromPermissionsApp from '../apps/permissions/shared/store/rootReducer';
 import projectAdminApp, * as fromProjectAdminApp from '../apps/project-admin/store/rootReducer';
-import qualityGatesApp from '../apps/quality-gates/store/rootReducer';
 import settingsApp, * as fromSettingsApp from '../apps/settings/store/rootReducer';
 
 export default combineReducers({
@@ -48,7 +47,6 @@ export default combineReducers({
   // apps
   permissionsApp,
   projectAdminApp,
-  qualityGatesApp,
   settingsApp
 });
 
@@ -124,8 +122,6 @@ export const getOrganizationMembersLogins = (state, organization) =>
 export const getOrganizationMembersState = (state, organization) =>
   fromOrganizationsMembers.getOrganizationMembersState(state.organizationsMembers, organization);
 
-export const getQualityGatesAppState = state => state.qualityGatesApp;
-
 export const getPermissionsAppUsers = state => fromPermissionsApp.getUsers(state.permissionsApp);
 
 export const getPermissionsAppGroups = state => fromPermissionsApp.getGroups(state.permissionsApp);
index 824627768bf5d2715338260e1ef2becfcbb94997..ce41b78b1fedc547832b986331ed9508181b8e9f 100644 (file)
@@ -1154,7 +1154,6 @@ quality_gates.no_conditions=No Conditions
 quality_gates.introduction=Only project measures are checked against thresholds. Sub-projects, directories and files are ignored.
 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.
-quality_gates.projects_for_default.edit=You must not select specific projects for the default quality gate.
 quality_gates.projects.with=With
 quality_gates.projects.without=Without
 quality_gates.projects.all=All