]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10151 Pass organization parameter in all quality gates ws
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 30 Nov 2017 16:22:26 +0000 (17:22 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Thu, 14 Dec 2017 16:03:35 +0000 (17:03 +0100)
27 files changed:
server/sonar-web/src/main/js/api/quality-gates.ts
server/sonar-web/src/main/js/apps/overview/components/App.js
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js
server/sonar-web/src/main/js/apps/overview/types.js
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/projectQualityGate/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Condition.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js
server/sonar-web/src/main/js/apps/quality-gates/components/DeleteConditionForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Details.js
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/store/actions.js
server/sonar-web/src/main/js/apps/quality-gates/store/rootReducer.js
server/sonar-web/src/main/js/apps/quality-gates/templates/quality-gates-condition-delete.hbs [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/views/gate-conditions-delete-view.js [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/views/gate-projects-view.js

index 90b03cbe338597dd9f15319e0947347bc05fc9d3..93fd2fd6e07f1e3ca3bbc9a4004e0bffaad845a6 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { getJSON, post, postJSON, RequestData } from '../helpers/request';
+import { getJSON, post, postJSON } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
 
-interface Condition {
-  error?: string;
-  id: number;
+export interface ConditionBase {
+  error: string;
   metric: string;
-  op: string;
+  op?: string;
   period?: number;
-  warning?: string;
+  warning: string;
+}
+
+export interface Condition extends ConditionBase {
+  id: number;
 }
 
 export interface QualityGate {
@@ -45,15 +48,20 @@ export interface QualityGate {
   name: string;
 }
 
-export function fetchQualityGates(): Promise<{
+export function fetchQualityGates(data: {
+  organization?: string;
+}): Promise<{
   actions: { create: boolean };
   qualitygates: QualityGate[];
 }> {
-  return getJSON('/api/qualitygates/list').catch(throwGlobalError);
+  return getJSON('/api/qualitygates/list', data).catch(throwGlobalError);
 }
 
-export function fetchQualityGate(id: number): Promise<QualityGate> {
-  return getJSON('/api/qualitygates/show', { id }).catch(throwGlobalError);
+export function fetchQualityGate(data: {
+  id: number;
+  organization?: string;
+}): Promise<QualityGate> {
+  return getJSON('/api/qualitygates/show', data).catch(throwGlobalError);
 }
 
 export function createQualityGate(data: {
@@ -63,8 +71,11 @@ export function createQualityGate(data: {
   return postJSON('/api/qualitygates/create', data).catch(throwGlobalError);
 }
 
-export function deleteQualityGate(id: number): Promise<void> {
-  return post('/api/qualitygates/destroy', { id });
+export function deleteQualityGate(data: {
+  id: number;
+  organization?: string;
+}): Promise<void | Response> {
+  return post('/api/qualitygates/destroy', data).catch(throwGlobalError);
 }
 
 export function renameQualityGate(data: {
@@ -83,46 +94,63 @@ export function copyQualityGate(data: {
   return postJSON('/api/qualitygates/copy', data).catch(throwGlobalError);
 }
 
-export function setQualityGateAsDefault(id: number): Promise<void | Response> {
-  return post('/api/qualitygates/set_as_default', { id }).catch(throwGlobalError);
+export function setQualityGateAsDefault(data: {
+  id: number;
+  organization?: string;
+}): Promise<void | Response> {
+  return post('/api/qualitygates/set_as_default', data).catch(throwGlobalError);
 }
 
-export function createCondition(gateId: number, condition: RequestData): Promise<any> {
-  return postJSON('/api/qualitygates/create_condition', { ...condition, gateId });
+export function createCondition(
+  data: {
+    gateId: number;
+    organization?: string;
+  } & ConditionBase
+): Promise<Condition> {
+  return postJSON('/api/qualitygates/create_condition', data);
 }
 
-export function updateCondition(condition: RequestData): Promise<any> {
-  return postJSON('/api/qualitygates/update_condition', condition);
+export function updateCondition(data: { organization?: string } & Condition): Promise<Condition> {
+  return postJSON('/api/qualitygates/update_condition', data);
 }
 
-export function deleteCondition(id: number): Promise<void> {
-  return post('/api/qualitygates/delete_condition', { id });
+export function deleteCondition(data: { id: number; organization?: string }): Promise<void> {
+  return post('/api/qualitygates/delete_condition', data);
 }
 
-export function getGateForProject(project: string): Promise<QualityGate | undefined> {
-  return getJSON('/api/qualitygates/get_by_project', { project }).then(
+export function getGateForProject(data: {
+  organization?: string;
+  project: string;
+}): Promise<QualityGate | undefined> {
+  return getJSON('/api/qualitygates/get_by_project', data).then(
     ({ qualityGate }) =>
       qualityGate && {
         ...qualityGate,
         isDefault: qualityGate.default
-      }
+      },
+    throwGlobalError
   );
 }
 
-export function associateGateWithProject(
-  gateId: number,
-  projectKey: string
-): Promise<void | Response> {
-  return post('/api/qualitygates/select', { gateId, projectKey }).catch(throwGlobalError);
+export function associateGateWithProject(data: {
+  gateId: number;
+  organization?: string;
+  projectKey: string;
+}): Promise<void | Response> {
+  return post('/api/qualitygates/select', data).catch(throwGlobalError);
 }
 
-export function dissociateGateWithProject(
-  gateId: number,
-  projectKey: string
-): Promise<void | Response> {
-  return post('/api/qualitygates/deselect', { gateId, projectKey }).catch(throwGlobalError);
+export function dissociateGateWithProject(data: {
+  gateId: number;
+  organization?: string;
+  projectKey: string;
+}): Promise<void | Response> {
+  return post('/api/qualitygates/deselect', data).catch(throwGlobalError);
 }
 
-export function getApplicationQualityGate(application: string): Promise<any> {
-  return getJSON('/api/qualitygates/application_status', { application });
+export function getApplicationQualityGate(data: {
+  application: string;
+  organization?: string;
+}): Promise<void | Response> {
+  return getJSON('/api/qualitygates/application_status', data).catch(throwGlobalError);
 }
index 8ce26596388cf203f209919570122038b2b86be0..cab5eacead8cb395c729d4291f7b8c0270c7af6e 100644 (file)
@@ -34,7 +34,8 @@ type Props = {
     id: string,
     key: string,
     qualifier: string,
-    tags: Array<string>
+    tags: Array<string>,
+    organization?: string
   },
   onComponentChange: {} => void,
   router: Object
index 8149f8963f46a9cdca5ec67c628e3d63ed2da659..92a248cec4a3b52a9c9a0b7c43dd7bb7805a877a 100644 (file)
@@ -27,7 +27,7 @@ import { translate } from '../../../helpers/l10n';
 
 /*::
 type Props = {
-  component: { key: string }
+  component: { key: string, organization?: string }
 };
 */
 
@@ -68,8 +68,12 @@ export default class ApplicationQualityGate extends React.PureComponent {
   }
 
   fetchDetails = () => {
+    const { component } = this.props;
     this.setState({ loading: true });
-    getApplicationQualityGate(this.props.component.key).then(
+    getApplicationQualityGate({
+      application: component.key,
+      organization: component.organization
+    }).then(
       ({ status, projects, metrics }) => {
         if (this.mounted) {
           this.setState({
index bc3c77aaea7b271e574c5ea8374ce421978d67fc..5a5cdb5df778a58a228f16fe9cf4bfa9b00f3e2b 100644 (file)
@@ -23,7 +23,8 @@
 export type Component = {
   id: string,
   key: string,
-  qualifier: string
+  qualifier: string,
+  organization?: string
 };
 */
 
index 95b899a9aa397877e6b408aef66d6768f48284b2..5297b4ecbe42216c1db15df8fffd6600a71e751a 100644 (file)
@@ -35,6 +35,7 @@ import { translate } from '../../helpers/l10n';
 
 interface Props {
   component: Component;
+  onComponentChange: (changes: {}) => void;
 }
 
 interface State {
@@ -67,8 +68,12 @@ export default class App extends React.PureComponent<Props> {
   }
 
   fetchQualityGates() {
+    const { component } = this.props;
     this.setState({ loading: true });
-    Promise.all([fetchQualityGates(), getGateForProject(this.props.component.key)]).then(
+    Promise.all([
+      fetchQualityGates({ organization: component.organization }),
+      getGateForProject({ organization: component.organization, project: component.key })
+    ]).then(
       ([{ qualitygates: allGates }, gate]) => {
         if (this.mounted) {
           this.setState({ allGates, gate, loading: false });
@@ -82,16 +87,21 @@ export default class App extends React.PureComponent<Props> {
     );
   }
 
-  handleChangeGate = (oldId: number | undefined, newId: number | undefined) => {
+  handleChangeGate = (oldId?: number, newId?: number) => {
     const { allGates } = this.state;
-
     if ((!oldId && !newId) || !allGates) {
       return Promise.resolve();
     }
 
+    const { component } = this.props;
+    const requestData = {
+      gateId: newId ? newId : oldId!,
+      organization: component.organization,
+      projectKey: component.key
+    };
     const request = newId
-      ? associateGateWithProject(newId, this.props.component.key)
-      : dissociateGateWithProject(oldId!, this.props.component.key);
+      ? associateGateWithProject(requestData)
+      : dissociateGateWithProject(requestData);
 
     return request.then(() => {
       if (this.mounted) {
@@ -100,6 +110,7 @@ export default class App extends React.PureComponent<Props> {
           const newGate = allGates.find(gate => gate.id === newId);
           if (newGate) {
             this.setState({ gate: newGate });
+            this.props.onComponentChange({ qualityGate: newGate });
           }
         } else {
           this.setState({ gate: undefined });
index 909767bfed937ff5860f281c8d2fac5be69a8b35..8b762e76f191a8b74775e028d6615da5d900538f 100644 (file)
@@ -25,7 +25,7 @@ import { translate } from '../../helpers/l10n';
 interface Props {
   allGates: QualityGate[];
   gate?: QualityGate;
-  onChange: (oldGate: number | undefined, newGate: number) => Promise<void>;
+  onChange: (oldGate?: number, newGate?: number) => Promise<void>;
 }
 
 interface State {
index d04a0ee2f6e8d145196ea5fcc601684b710060b8..2252afd252e6851bdb2db820fe974173940d08c4 100644 (file)
@@ -72,16 +72,18 @@ beforeEach(() => {
 
 it('checks permissions', () => {
   handleRequiredAuthorization.mockClear();
-  mount(<App component={{ ...component, configuration: undefined }} />);
+  mount(
+    <App component={{ ...component, configuration: undefined }} onComponentChange={jest.fn()} />
+  );
   expect(handleRequiredAuthorization).toBeCalled();
 });
 
 it('fetches quality gates', () => {
   fetchQualityGates.mockClear();
   getGateForProject.mockClear();
-  mount(<App component={component} />);
-  expect(fetchQualityGates).toBeCalledWith();
-  expect(getGateForProject).toBeCalledWith('component');
+  mount(<App component={component} onComponentChange={jest.fn()} />);
+  expect(fetchQualityGates).toBeCalledWith({ organization: 'org' });
+  expect(getGateForProject).toBeCalledWith({ organization: 'org', project: 'component' });
 });
 
 it('changes quality gate from custom to default', () => {
@@ -89,28 +91,44 @@ it('changes quality gate from custom to default', () => {
   const allGates = [gate, randomGate('bar', true), randomGate('baz')];
   const wrapper = mountRender(allGates, gate);
   wrapper.find('Form').prop<Function>('onChange')('foo', 'bar');
-  expect(associateGateWithProject).toBeCalledWith('bar', 'component');
+  expect(associateGateWithProject).toBeCalledWith({
+    gateId: 'bar',
+    organization: 'org',
+    projectKey: 'component'
+  });
 });
 
 it('changes quality gate from custom to custom', () => {
   const allGates = [randomGate('foo'), randomGate('bar', true), randomGate('baz')];
   const wrapper = mountRender(allGates, randomGate('foo'));
   wrapper.find('Form').prop<Function>('onChange')('foo', 'baz');
-  expect(associateGateWithProject).toBeCalledWith('baz', 'component');
+  expect(associateGateWithProject).toBeCalledWith({
+    gateId: 'baz',
+    organization: 'org',
+    projectKey: 'component'
+  });
 });
 
 it('changes quality gate from custom to none', () => {
   const allGates = [randomGate('foo'), randomGate('bar'), randomGate('baz')];
   const wrapper = mountRender(allGates, randomGate('foo'));
   wrapper.find('Form').prop<Function>('onChange')('foo', undefined);
-  expect(dissociateGateWithProject).toBeCalledWith('foo', 'component');
+  expect(dissociateGateWithProject).toBeCalledWith({
+    gateId: 'foo',
+    organization: 'org',
+    projectKey: 'component'
+  });
 });
 
 it('changes quality gate from none to custom', () => {
   const allGates = [randomGate('foo'), randomGate('bar'), randomGate('baz')];
   const wrapper = mountRender(allGates);
   wrapper.find('Form').prop<Function>('onChange')(undefined, 'baz');
-  expect(associateGateWithProject).toBeCalledWith('baz', 'component');
+  expect(associateGateWithProject).toBeCalledWith({
+    gateId: 'baz',
+    organization: 'org',
+    projectKey: 'component'
+  });
 });
 
 function randomGate(id: string, isDefault = false) {
@@ -118,7 +136,7 @@ function randomGate(id: string, isDefault = false) {
 }
 
 function mountRender(allGates: any[], gate?: any) {
-  const wrapper = mount(<App component={component} />);
+  const wrapper = mount(<App component={component} onComponentChange={jest.fn()} />);
   wrapper.setState({ allGates, loading: false, gate });
   return wrapper;
 }
index 5907e9d577061bf4b96946197d534223adcef9a3..7496c7cefe179db8f9b28375a334c8e8c95be349 100644 (file)
@@ -36,8 +36,10 @@ export default function BuiltInQualityGateBadge({ className, tooltip = true }: P
 
   const overlay = (
     <div>
-      <p>{translate('quality_gates.built_in.description.1')}</p>
-      <p>{translate('quality_gates.built_in.description.2')}</p>
+      <span>{translate('quality_gates.built_in.description.1')}</span>
+      <span className="little-spacer-left">
+        {translate('quality_gates.built_in.description.2')}
+      </span>
     </div>
   );
 
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.js
deleted file mode 100644 (file)
index c668901..0000000
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 ThresholdInput from './ThresholdInput';
-import DeleteConditionView from '../views/gate-conditions-delete-view';
-import Checkbox from '../../../components/controls/Checkbox';
-import { createCondition, updateCondition } from '../../../api/quality-gates';
-import Select from '../../../components/controls/Select';
-import { translate, getLocalizedMetricName } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
-
-export default class Condition extends Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      changed: false,
-      period: props.condition.period,
-      op: props.condition.op,
-      warning: props.condition.warning || '',
-      error: props.condition.error || ''
-    };
-  }
-
-  componentDidMount() {
-    if (!this.props.condition.id && this.operator) {
-      this.operator.focus();
-    }
-  }
-
-  handleOperatorChange = ({ value }) => {
-    this.setState({ changed: true, op: value });
-  };
-
-  handlePeriodChange = checked => {
-    const period = checked ? '1' : undefined;
-    this.setState({ changed: true, period });
-  };
-
-  handleWarningChange = value => {
-    this.setState({ changed: true, warning: value });
-  };
-
-  handleErrorChange = value => {
-    this.setState({ changed: true, error: value });
-  };
-
-  handleSaveClick = e => {
-    const { qualityGate, condition, metric, onSaveCondition, onError, onResetError } = this.props;
-    const { period } = this.state;
-    const data = {
-      metric: condition.metric,
-      op: metric.type === 'RATING' ? 'GT' : this.state.op,
-      warning: this.state.warning,
-      error: this.state.error
-    };
-
-    if (period && metric.type !== 'RATING') {
-      data.period = period;
-    }
-
-    if (metric.key.indexOf('new_') === 0) {
-      data.period = '1';
-    }
-
-    e.preventDefault();
-    createCondition(qualityGate.id, data)
-      .then(newCondition => {
-        this.setState({ changed: false });
-        onSaveCondition(condition, newCondition);
-        onResetError();
-      })
-      .catch(onError);
-  };
-
-  handleUpdateClick = e => {
-    const { condition, onSaveCondition, metric, onError, onResetError } = this.props;
-    const { period } = this.state;
-    const data = {
-      id: condition.id,
-      metric: condition.metric,
-      op: metric.type === 'RATING' ? 'GT' : this.state.op,
-      warning: this.state.warning,
-      error: this.state.error
-    };
-
-    if (period && metric.type !== 'RATING') {
-      data.period = period;
-    }
-
-    if (metric.key.indexOf('new_') === 0) {
-      data.period = '1';
-    }
-
-    e.preventDefault();
-    updateCondition(data)
-      .then(newCondition => {
-        this.setState({ changed: false });
-        onSaveCondition(condition, newCondition);
-        onResetError();
-      })
-      .catch(onError);
-  };
-
-  handleDeleteClick = e => {
-    const { qualityGate, condition, metric, onDeleteCondition } = this.props;
-
-    e.preventDefault();
-    new DeleteConditionView({
-      qualityGate,
-      condition,
-      metric,
-      onDelete: () => onDeleteCondition(condition)
-    }).render();
-  };
-
-  handleCancelClick = e => {
-    const { condition, onDeleteCondition } = this.props;
-
-    e.preventDefault();
-    onDeleteCondition(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) {
-      return (
-        <span className="note">{translate('quality_gates.condition.leak.unconditional')}</span>
-      );
-    }
-
-    if (isRating) {
-      return <span className="note">{translate('quality_gates.condition.leak.never')}</span>;
-    }
-
-    return isLeakSelected
-      ? translate('quality_gates.condition.leak.yes')
-      : translate('quality_gates.condition.leak.no');
-  }
-
-  renderPeriod() {
-    const { condition, metric, edit } = this.props;
-
-    const isDiffMetric = condition.metric.indexOf('new_') === 0;
-    const isRating = metric.type === 'RATING';
-    const isLeakSelected = !!this.state.period;
-
-    if (isRating || isDiffMetric || !edit) {
-      return this.renderPeriodValue();
-    }
-
-    return <Checkbox checked={isLeakSelected} onCheck={this.handlePeriodChange} />;
-  }
-
-  renderOperator() {
-    const { condition, edit, metric } = this.props;
-
-    if (!edit) {
-      return metric.type === 'RATING'
-        ? translate('quality_gates.operator', condition.op, 'rating')
-        : translate('quality_gates.operator', condition.op);
-    }
-
-    if (metric.type === 'RATING') {
-      return <span className="note">{translate('quality_gates.operator.GT.rating')}</span>;
-    }
-
-    const operators = ['LT', 'GT', 'EQ', 'NE'];
-    const operatorOptions = operators.map(op => {
-      const label = translate('quality_gates.operator', op);
-      return { label, value: op };
-    });
-
-    return (
-      <Select
-        className="input-medium"
-        clearable={false}
-        innerRef={node => (this.operator = node)}
-        name="operator"
-        onChange={this.handleOperatorChange}
-        options={operatorOptions}
-        searchable={false}
-        value={this.state.op}
-      />
-    );
-  }
-
-  render() {
-    const { condition, edit, metric } = this.props;
-    return (
-      <tr>
-        <td className="text-middle">
-          {getLocalizedMetricName(metric)}
-          {metric.hidden && (
-            <span className="text-danger little-spacer-left">{translate('deprecated')}</span>
-          )}
-        </td>
-
-        <td className="thin text-middle nowrap">{this.renderPeriod()}</td>
-
-        <td className="thin text-middle nowrap">{this.renderOperator()}</td>
-
-        <td className="thin text-middle nowrap">
-          {edit ? (
-            <ThresholdInput
-              name="warning"
-              value={this.state.warning}
-              metric={metric}
-              onChange={this.handleWarningChange}
-            />
-          ) : (
-            formatMeasure(condition.warning, metric.type)
-          )}
-        </td>
-
-        <td className="thin text-middle nowrap">
-          {edit ? (
-            <ThresholdInput
-              name="error"
-              value={this.state.error}
-              metric={metric}
-              onChange={this.handleErrorChange}
-            />
-          ) : (
-            formatMeasure(condition.error, metric.type)
-          )}
-        </td>
-
-        {edit && (
-          <td className="thin text-middle nowrap">
-            {condition.id ? (
-              <div>
-                <button
-                  className="update-condition"
-                  disabled={!this.state.changed}
-                  onClick={this.handleUpdateClick}>
-                  {translate('update_verb')}
-                </button>
-                <button
-                  className="button-red delete-condition little-spacer-left"
-                  onClick={this.handleDeleteClick}>
-                  {translate('delete')}
-                </button>
-              </div>
-            ) : (
-              <div>
-                <button className="add-condition" onClick={this.handleSaveClick}>
-                  {translate('add_verb')}
-                </button>
-                <a
-                  className="cancel-add-condition spacer-left"
-                  href="#"
-                  onClick={this.handleCancelClick}>
-                  {translate('cancel')}
-                </a>
-              </div>
-            )}
-          </td>
-        )}
-      </tr>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx
new file mode 100644 (file)
index 0000000..741a7b2
--- /dev/null
@@ -0,0 +1,297 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 Checkbox from '../../../components/controls/Checkbox';
+import DeleteConditionForm from './DeleteConditionForm';
+import Select from '../../../components/controls/Select';
+import ThresholdInput from './ThresholdInput';
+import {
+  Condition as ICondition,
+  ConditionBase,
+  createCondition,
+  QualityGate,
+  updateCondition
+} from '../../../api/quality-gates';
+import { Metric } from '../../../app/types';
+import { translate, getLocalizedMetricName } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+
+interface Props {
+  condition: ICondition;
+  edit: boolean;
+  metric: Metric;
+  organization: string;
+  onDeleteCondition: (condition: ICondition) => void;
+  onError: (error: any) => void;
+  onResetError: () => void;
+  onSaveCondition: (condition: ICondition, newCondition: ICondition) => void;
+  qualityGate: QualityGate;
+}
+
+interface State {
+  changed: boolean;
+  period?: number;
+  op?: string;
+  openDeleteCondition: boolean;
+  warning: string;
+  error: string;
+}
+
+export default class Condition extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.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,
+      op: metric.type === 'RATING' ? 'GT' : this.state.op,
+      warning: this.state.warning,
+      error: this.state.error
+    };
+
+    if (period && metric.type !== 'RATING') {
+      data.period = period;
+    }
+
+    if (metric.key.indexOf('new_') === 0) {
+      data.period = 1;
+    }
+
+    createCondition({ gateId: qualityGate.id, organization, ...data }).then(
+      this.handleConditionResponse,
+      this.props.onError
+    );
+  };
+
+  handleUpdateClick = () => {
+    const { condition, metric, organization } = this.props;
+    const { period } = this.state;
+    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
+    };
+
+    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
+    );
+  };
+
+  handleConditionResponse = (newCondition: ICondition) => {
+    this.setState({ changed: false });
+    this.props.onSaveCondition(this.props.condition, newCondition);
+    this.props.onResetError();
+  };
+
+  handleCancelClick = (e: React.SyntheticEvent<HTMLAnchorElement>) => {
+    e.preventDefault();
+    e.stopPropagation();
+    this.props.onDeleteCondition(this.props.condition);
+  };
+
+  openDeleteConditionForm = () => this.setState({ openDeleteCondition: true });
+  closeDeleteConditionForm = () => this.setState({ openDeleteCondition: false });
+
+  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) {
+      return (
+        <span className="note">{translate('quality_gates.condition.leak.unconditional')}</span>
+      );
+    }
+
+    if (isRating) {
+      return <span className="note">{translate('quality_gates.condition.leak.never')}</span>;
+    }
+
+    return isLeakSelected
+      ? translate('quality_gates.condition.leak.yes')
+      : translate('quality_gates.condition.leak.no');
+  }
+
+  renderPeriod() {
+    const { condition, metric, edit } = this.props;
+
+    const isDiffMetric = condition.metric.indexOf('new_') === 0;
+    const isRating = metric.type === 'RATING';
+    const isLeakSelected = !!this.state.period;
+
+    if (isRating || isDiffMetric || !edit) {
+      return this.renderPeriodValue();
+    }
+
+    return <Checkbox checked={isLeakSelected} onCheck={this.handlePeriodChange} />;
+  }
+
+  renderOperator() {
+    const { condition, edit, metric } = this.props;
+
+    if (!edit && condition.op) {
+      return metric.type === 'RATING'
+        ? translate('quality_gates.operator', condition.op, 'rating')
+        : translate('quality_gates.operator', condition.op);
+    }
+
+    if (metric.type === 'RATING') {
+      return <span className="note">{translate('quality_gates.operator.GT.rating')}</span>;
+    }
+
+    const operators = ['LT', 'GT', 'EQ', 'NE'];
+    const operatorOptions = operators.map(op => {
+      const label = translate('quality_gates.operator', op);
+      return { label, value: op };
+    });
+
+    return (
+      <Select
+        autofocus={true}
+        className="input-medium"
+        clearable={false}
+        name="operator"
+        onChange={this.handleOperatorChange}
+        options={operatorOptions}
+        searchable={false}
+        value={this.state.op}
+      />
+    );
+  }
+
+  render() {
+    const { condition, edit, metric, organization } = this.props;
+    return (
+      <tr>
+        <td className="text-middle">
+          {getLocalizedMetricName(metric)}
+          {metric.hidden && (
+            <span className="text-danger little-spacer-left">{translate('deprecated')}</span>
+          )}
+        </td>
+
+        <td className="thin text-middle nowrap">{this.renderPeriod()}</td>
+
+        <td className="thin text-middle nowrap">{this.renderOperator()}</td>
+
+        <td className="thin text-middle nowrap">
+          {edit ? (
+            <ThresholdInput
+              name="warning"
+              value={this.state.warning}
+              metric={metric}
+              onChange={this.handleWarningChange}
+            />
+          ) : (
+            formatMeasure(condition.warning, metric.type)
+          )}
+        </td>
+
+        <td className="thin text-middle nowrap">
+          {edit ? (
+            <ThresholdInput
+              name="error"
+              value={this.state.error}
+              metric={metric}
+              onChange={this.handleErrorChange}
+            />
+          ) : (
+            formatMeasure(condition.error, metric.type)
+          )}
+        </td>
+
+        {edit && (
+          <td className="thin text-middle nowrap">
+            {condition.id ? (
+              <div>
+                <button
+                  className="update-condition"
+                  disabled={!this.state.changed}
+                  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}
+                  />
+                )}
+              </div>
+            ) : (
+              <div>
+                <button className="add-condition" onClick={this.handleSaveClick}>
+                  {translate('add_verb')}
+                </button>
+                <a
+                  className="cancel-add-condition spacer-left"
+                  href="#"
+                  onClick={this.handleCancelClick}>
+                  {translate('cancel')}
+                </a>
+              </div>
+            )}
+          </td>
+        )}
+      </tr>
+    );
+  }
+}
index 7986f5a1816fbfd90cf7b032201f119a31254bb4..547543cb215f7c23625fd80f5232320508a30db1 100644 (file)
@@ -62,7 +62,8 @@ export default class Conditions extends React.PureComponent {
       edit,
       onAddCondition,
       onSaveCondition,
-      onDeleteCondition
+      onDeleteCondition,
+      organization
     } = this.props;
 
     const existingConditions = conditions.filter(condition => metrics[condition.metric]);
@@ -130,6 +131,7 @@ export default class Conditions extends React.PureComponent {
                   onDeleteCondition={onDeleteCondition}
                   onError={this.handleError.bind(this)}
                   onResetError={this.handleResetError.bind(this)}
+                  organization={organization}
                 />
               ))}
             </tbody>
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteConditionForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteConditionForm.tsx
new file mode 100644 (file)
index 0000000..c4d1ea5
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+* SonarQube
+* Copyright (C) 2009-2017 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 Modal from '../../../components/controls/Modal';
+import { Metric } from '../../../app/types';
+import { Condition, deleteCondition } from '../../../api/quality-gates';
+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: boolean;
+  state: State = { loading: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleCancelClick = (event: React.SyntheticEvent<HTMLElement>) => {
+    event.preventDefault();
+    this.props.onClose();
+  };
+
+  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    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 });
+        }
+      }
+    );
+  };
+
+  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" />}
+            <button className="js-delete button-red" disabled={this.state.loading}>
+              {translate('delete')}
+            </button>
+            <a href="#" className="js-modal-close" onClick={this.handleCancelClick}>
+              {translate('cancel')}
+            </a>
+          </div>
+        </form>
+      </Modal>
+    );
+  }
+}
index ab80d7e9df9cfdcaf5106a8163b77ca702cf5ec4..6bb533c415c85788b21e6cfd8773c379984ff673 100644 (file)
@@ -62,7 +62,7 @@ export default class DeleteQualityGateForm extends React.PureComponent<Props, St
     event.preventDefault();
     const { organization, qualityGate } = this.props;
     this.setState({ loading: true });
-    deleteQualityGate(qualityGate.id).then(
+    deleteQualityGate({ id: qualityGate.id, organization }).then(
       () => {
         this.props.onDelete(qualityGate);
         this.context.router.replace(getQualityGatesUrl(organization));
index b6ff64b5c874fdc5a8697a0e086328bf294b9ec1..a231079b6a2368c24dd8f735080a06f184f2a260 100644 (file)
@@ -20,7 +20,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import Helmet from 'react-helmet';
-import { fetchQualityGate, setQualityGateAsDefault } from '../../../api/quality-gates';
+import { fetchQualityGate } from '../../../api/quality-gates';
 import DetailsHeader from './DetailsHeader';
 import DetailsContent from './DetailsContent';
 
@@ -41,10 +41,10 @@ export default class Details extends React.PureComponent {
   }
 
   fetchDetails = () =>
-    fetchQualityGate(this.props.params.id).then(
-      qualityGate => this.props.onShow(qualityGate),
-      () => {}
-    );
+    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;
@@ -72,6 +72,7 @@ export default class Details extends React.PureComponent {
           onAddCondition={onAddCondition}
           onSaveCondition={onSaveCondition}
           onDeleteCondition={onDeleteCondition}
+          organization={organization && organization.key}
         />
       </div>
     );
index 0489459a58c95266522024b243800c043443f34d..8a424be367e34b717fd989ab9a62dafbd1a91322 100644 (file)
@@ -24,7 +24,7 @@ import { translate } from '../../../helpers/l10n';
 
 export default class DetailsContent extends React.PureComponent {
   render() {
-    const { gate, metrics } = this.props;
+    const { gate, metrics, organization } = this.props;
     const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props;
     const conditions = gate.conditions || [];
     const actions = gate.actions || {};
@@ -43,6 +43,7 @@ export default class DetailsContent extends React.PureComponent {
           onAddCondition={onAddCondition}
           onSaveCondition={onSaveCondition}
           onDeleteCondition={onDeleteCondition}
+          organization={organization}
         />
 
         <div id="quality-gate-projects" className="quality-gate-section">
@@ -50,7 +51,11 @@ export default class DetailsContent extends React.PureComponent {
           {gate.isDefault ? (
             defaultMessage
           ) : (
-            <Projects qualityGate={gate} edit={actions.associateProjects} />
+            <Projects
+              qualityGate={gate}
+              edit={actions.associateProjects}
+              organization={organization}
+            />
           )}
         </div>
       </div>
index aa3d836b2376ad515ec7b8e71531e73d81b80b81..f781cc253b896e9adb600e2d94792c568335fb02 100644 (file)
@@ -22,7 +22,7 @@ import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
 import RenameQualityGateForm from './RenameQualityGateForm';
 import CopyQualityGateForm from './CopyQualityGateForm';
 import DeleteQualityGateForm from './DeleteQualityGateForm';
-import { QualityGate, setQualityGateAsDefault } from '../../../api/quality-gates';
+import { fetchQualityGate, QualityGate, setQualityGateAsDefault } from '../../../api/quality-gates';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
@@ -53,9 +53,11 @@ export default class DetailsHeader extends React.PureComponent<Props, State> {
 
   handleSetAsDefaultClick = (e: React.SyntheticEvent<HTMLButtonElement>) => {
     e.preventDefault();
-    const { qualityGate, onSetAsDefault } = this.props;
+    const { qualityGate, onSetAsDefault, organization } = this.props;
     if (!qualityGate.isDefault) {
-      setQualityGateAsDefault(qualityGate.id).then(() => onSetAsDefault(qualityGate), () => {});
+      setQualityGateAsDefault({ id: qualityGate.id, organization })
+        .then(() => fetchQualityGate({ id: qualityGate.id, organization }))
+        .then(qualityGate => onSetAsDefault(qualityGate), () => {});
     }
   };
 
index 33a9c447d1319db31754c100bc86c8ab28530e79..344bf1e2881031a5806e8db30f0b98c566c107a5 100644 (file)
@@ -44,12 +44,13 @@ export default class Projects extends React.PureComponent {
   }
 
   renderView() {
-    const { qualityGate, edit } = this.props;
+    const { qualityGate, edit, organization } = this.props;
 
     this.projectsView = new ProjectsView({
       qualityGate,
       edit,
-      container: this.refs.container
+      container: this.refs.container,
+      organization
     });
     this.projectsView.render();
   }
index 96c3ed76f9ab34288f2ad6cef5f069af1906c54d..0ec8d28794d832df7f060fd01a367ada29e4122f 100644 (file)
@@ -51,15 +51,20 @@ export default class QualityGatesApp extends Component {
   }
 
   fetchQualityGates = () =>
-    fetchQualityGates().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)
-        );
-      }
-    });
+    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;
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.js b/server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.js
deleted file mode 100644 (file)
index b00a7e8..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 Select from '../../../components/controls/Select';
-
-export default class ThresholdInput extends React.PureComponent {
-  static propTypes = {
-    name: PropTypes.string.isRequired,
-    value: PropTypes.any,
-    metric: PropTypes.object.isRequired,
-    onChange: PropTypes.func.isRequired
-  };
-
-  handleChange = e => {
-    this.props.onChange(e.target.value);
-  };
-
-  handleSelectChange = option => {
-    if (option) {
-      this.props.onChange(option.value);
-    } else {
-      this.props.onChange('');
-    }
-  };
-
-  renderRatingInput() {
-    const { name, value } = this.props;
-
-    const options = [
-      { label: 'A', value: '1' },
-      { label: 'B', value: '2' },
-      { label: 'C', value: '3' },
-      { label: 'D', value: '4' }
-    ];
-
-    const realValue = value === '' ? null : value;
-
-    return (
-      <Select
-        className="input-tiny text-middle"
-        name={name}
-        value={realValue}
-        options={options}
-        searchable={false}
-        placeholder=""
-        onChange={this.handleSelectChange}
-      />
-    );
-  }
-
-  render() {
-    const { name, value, metric } = this.props;
-
-    if (metric.type === 'RATING') {
-      return this.renderRatingInput();
-    }
-
-    return (
-      <input
-        name={name}
-        type="text"
-        className="input-tiny text-middle"
-        value={value}
-        data-type={metric.type}
-        placeholder={metric.placeholder}
-        onChange={this.handleChange}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.tsx
new file mode 100644 (file)
index 0000000..cf60765
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 Select from '../../../components/controls/Select';
+import { Metric } from '../../../app/types';
+
+interface Props {
+  name: string;
+  value: string;
+  metric: Metric;
+  onChange: (value: string) => void;
+}
+
+export default class ThresholdInput extends React.PureComponent<Props> {
+  handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
+    this.props.onChange(e.currentTarget.value);
+  };
+
+  handleSelectChange = (option: any) => {
+    if (option) {
+      this.props.onChange(option.value);
+    } else {
+      this.props.onChange('');
+    }
+  };
+
+  renderRatingInput() {
+    const { name, value } = this.props;
+
+    const options = [
+      { label: 'A', value: '1' },
+      { label: 'B', value: '2' },
+      { label: 'C', value: '3' },
+      { label: 'D', value: '4' }
+    ];
+
+    return (
+      <Select
+        className="input-tiny text-middle"
+        name={name}
+        onChange={this.handleSelectChange}
+        options={options}
+        placeholder=""
+        searchable={false}
+        value={value}
+      />
+    );
+  }
+
+  render() {
+    const { name, value, metric } = this.props;
+
+    if (metric.type === 'RATING') {
+      return this.renderRatingInput();
+    }
+
+    return (
+      <input
+        name={name}
+        type="text"
+        className="input-tiny text-middle"
+        value={value}
+        data-type={metric.type}
+        onChange={this.handleChange}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.js b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.js
deleted file mode 100644 (file)
index 3272739..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 { shallow } from 'enzyme';
-import ThresholdInput from '../ThresholdInput';
-import { change } from '../../../../helpers/testUtils';
-
-describe('on strings', () => {
-  it('should render text input', () => {
-    const input = shallow(
-      <ThresholdInput name="foo" value="2" metric={{ type: 'INTEGER' }} onChange={jest.fn()} />
-    ).find('input');
-    expect(input.length).toEqual(1);
-    expect(input.prop('name')).toEqual('foo');
-    expect(input.prop('value')).toEqual('2');
-  });
-
-  it('should change', () => {
-    const onChange = jest.fn();
-    const input = shallow(
-      <ThresholdInput name="foo" value="2" metric={{ type: 'INTEGER' }} onChange={onChange} />
-    ).find('input');
-    change(input, 'bar');
-    expect(onChange).toBeCalledWith('bar');
-  });
-});
-
-describe('on ratings', () => {
-  it('should render Select', () => {
-    const select = shallow(
-      <ThresholdInput name="foo" value="2" metric={{ type: 'RATING' }} onChange={jest.fn()} />
-    ).find('Select');
-    expect(select.length).toEqual(1);
-    expect(select.prop('value')).toEqual('2');
-  });
-
-  it('should set', () => {
-    const onChange = jest.fn();
-    const select = shallow(
-      <ThresholdInput name="foo" value="2" metric={{ type: 'RATING' }} onChange={onChange} />
-    ).find('Select');
-    select.prop('onChange')({ label: 'D', value: '4' });
-    expect(onChange).toBeCalledWith('4');
-  });
-
-  it('should unset', () => {
-    const onChange = jest.fn();
-    const select = shallow(
-      <ThresholdInput name="foo" value="2" metric={{ type: 'RATING' }} onChange={onChange} />
-    ).find('Select');
-    select.prop('onChange')(null);
-    expect(onChange).toBeCalledWith('');
-  });
-});
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx
new file mode 100644 (file)
index 0000000..42f29a6
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { shallow } from 'enzyme';
+import ThresholdInput from '../ThresholdInput';
+import { change } from '../../../../helpers/testUtils';
+
+describe('on strings', () => {
+  const metric = { key: 'foo', name: 'Foo', type: 'INTEGER' };
+  it('should render text input', () => {
+    const input = shallow(
+      <ThresholdInput name="foo" value="2" metric={metric} onChange={jest.fn()} />
+    ).find('input');
+    expect(input.length).toEqual(1);
+    expect(input.prop('name')).toEqual('foo');
+    expect(input.prop('value')).toEqual('2');
+  });
+
+  it('should change', () => {
+    const onChange = jest.fn();
+    const input = shallow(
+      <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} />
+    ).find('input');
+    change(input, 'bar');
+    expect(onChange).toBeCalledWith('bar');
+  });
+});
+
+describe('on ratings', () => {
+  const metric = { key: 'foo', name: 'Foo', type: 'RATING' };
+  it('should render Select', () => {
+    const select = shallow(
+      <ThresholdInput name="foo" value="2" metric={metric} onChange={jest.fn()} />
+    ).find('Select');
+    expect(select.length).toEqual(1);
+    expect(select.prop('value')).toEqual('2');
+  });
+
+  it('should set', () => {
+    const onChange = jest.fn();
+    const select = shallow(
+      <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} />
+    ).find('Select');
+    (select.prop('onChange') as Function)({ label: 'D', value: '4' });
+    expect(onChange).toBeCalledWith('4');
+  });
+
+  it('should unset', () => {
+    const onChange = jest.fn();
+    const select = shallow(
+      <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} />
+    ).find('Select');
+    (select.prop('onChange') as Function)(null);
+    expect(onChange).toBeCalledWith('');
+  });
+});
index ad94c833969baf66d8f02fd8de57ef639b1024fb..e8b688273b209ddccf246806450a5f4ebdd4dec2 100644 (file)
@@ -66,7 +66,7 @@ export function copyQualityGate(qualityGate) {
   };
 }
 
-export const SET_AS_DEFAULT = 'SET_AS_DEFAULT';
+export const SET_AS_DEFAULT = 'qualityGates/SET_AS_DEFAULT';
 export function setQualityGateAsDefault(qualityGate) {
   return {
     type: SET_AS_DEFAULT,
index 56078fc6518a4c89a41561fc6a7b71395cc4fe91..b63d134e2ac757e64ac91916f977bae3605e727d 100644 (file)
@@ -70,7 +70,7 @@ export default function rootReducer(state = initialState, action = {}) {
           return { ...candidate, isDefault: candidate.id === action.qualityGate.id };
         }),
         qualityGate: {
-          ...state.qualityGate,
+          ...action.qualityGate,
           isDefault: state.qualityGate.id === action.qualityGate.id
         }
       };
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/templates/quality-gates-condition-delete.hbs b/server/sonar-web/src/main/js/apps/quality-gates/templates/quality-gates-condition-delete.hbs
deleted file mode 100644 (file)
index 5ca1ac2..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<form id="delete-condition-form">
-  <div class="modal-head">
-    <h2>{{t 'quality_gates.delete_condition'}}</h2>
-  </div>
-  <div class="modal-body">
-    <div class="js-modal-messages"></div>
-    {{tp 'quality_gates.delete_condition.confirm.message' localizedMetricName}}
-  </div>
-  <div class="modal-foot">
-    <button id="delete-condition-submit">{{t 'delete'}}</button>
-    <a href="#" class="js-modal-close" id="delete-condition-cancel">{{t 'cancel'}}</a>
-  </div>
-</form>
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/views/gate-conditions-delete-view.js b/server/sonar-web/src/main/js/apps/quality-gates/views/gate-conditions-delete-view.js
deleted file mode 100644 (file)
index f9f01ac..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 ModalForm from '../../../components/common/modal-form';
-import Template from '../templates/quality-gates-condition-delete.hbs';
-import { deleteCondition } from '../../../api/quality-gates';
-import { getLocalizedMetricName } from '../../../helpers/l10n';
-import { parseError } from '../../../helpers/request';
-
-export default ModalForm.extend({
-  template: Template,
-
-  onFormSubmit() {
-    ModalForm.prototype.onFormSubmit.apply(this, arguments);
-    this.disableForm();
-    this.sendRequest();
-  },
-
-  sendRequest() {
-    return deleteCondition(this.options.condition.id).then(
-      () => {
-        this.destroy();
-        this.options.onDelete();
-      },
-      error => {
-        this.enableForm();
-        parseError(error).then(msg => this.showErrors([{ msg }]));
-      }
-    );
-  },
-
-  serializeData() {
-    return {
-      metric: this.options.metric,
-      localizedMetricName: getLocalizedMetricName(this.options.metric)
-    };
-  }
-});
index 1b54745a857d90df1ba9de65ca8cf70ecfc32b79..6c648020b9981c64a939916cd12ab263987b1a0e 100644 (file)
@@ -25,18 +25,16 @@ import { translate } from '../../../helpers/l10n';
 export default Marionette.ItemView.extend({
   template: () => {},
 
-  initialize(options) {
-    this.organization = options.organization;
-  },
-
   onRender() {
-    const { qualityGate } = this.options;
+    const { qualityGate, organization } = this.options;
 
     const extra = {
       gateId: qualityGate.id
     };
-    if (this.organization) {
-      extra.organization = this.organization.key;
+    let orgQuery = '';
+    if (organization) {
+      extra.organization = organization;
+      orgQuery = '&organization=' + organization;
     }
 
     new SelectList({
@@ -47,7 +45,7 @@ export default Marionette.ItemView.extend({
       dangerouslyUnescapedHtmlFormat(item) {
         return escapeHtml(item.name);
       },
-      searchUrl: window.baseUrl + '/api/qualitygates/search?gateId=' + qualityGate.id,
+      searchUrl: `${window.baseUrl}/api/qualitygates/search?gateId=${qualityGate.id}${orgQuery}`,
       selectUrl: window.baseUrl + '/api/qualitygates/select',
       deselectUrl: window.baseUrl + '/api/qualitygates/deselect',
       extra,