diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps/quality-gates/components')
12 files changed, 247 insertions, 116 deletions
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx index 5907e9d5770..7496c7cefe1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/BuiltInQualityGateBadge.tsx @@ -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.tsx index c66890153f3..741a7b2efd0 100644 --- 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.tsx @@ -17,54 +17,71 @@ * 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 * as React from 'react'; import Checkbox from '../../../components/controls/Checkbox'; -import { createCondition, updateCondition } from '../../../api/quality-gates'; +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'; -export default class Condition extends Component { - constructor(props) { +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 || '' }; } - componentDidMount() { - if (!this.props.condition.id && this.operator) { - this.operator.focus(); - } - } + handleOperatorChange = ({ value }: any) => this.setState({ changed: true, op: value }); - handleOperatorChange = ({ value }) => { - this.setState({ changed: true, op: value }); - }; - - handlePeriodChange = checked => { - const period = checked ? '1' : undefined; + handlePeriodChange = (checked: boolean) => { + const period = checked ? 1 : undefined; this.setState({ changed: true, period }); }; - handleWarningChange = value => { - this.setState({ changed: true, warning: value }); - }; + handleWarningChange = (warning: string) => this.setState({ changed: true, warning }); - handleErrorChange = value => { - this.setState({ changed: true, error: value }); - }; + handleErrorChange = (error: string) => this.setState({ changed: true, error }); - handleSaveClick = e => { - const { qualityGate, condition, metric, onSaveCondition, onError, onResetError } = this.props; + handleSaveClick = () => { + const { qualityGate, condition, metric, organization } = this.props; const { period } = this.state; - const data = { + const data: ConditionBase = { metric: condition.metric, op: metric.type === 'RATING' ? 'GT' : this.state.op, warning: this.state.warning, @@ -76,23 +93,19 @@ export default class Condition extends Component { } if (metric.key.indexOf('new_') === 0) { - data.period = '1'; + data.period = 1; } - e.preventDefault(); - createCondition(qualityGate.id, data) - .then(newCondition => { - this.setState({ changed: false }); - onSaveCondition(condition, newCondition); - onResetError(); - }) - .catch(onError); + createCondition({ gateId: qualityGate.id, organization, ...data }).then( + this.handleConditionResponse, + this.props.onError + ); }; - handleUpdateClick = e => { - const { condition, onSaveCondition, metric, onError, onResetError } = this.props; + handleUpdateClick = () => { + const { condition, metric, organization } = this.props; const { period } = this.state; - const data = { + const data: ICondition = { id: condition.id, metric: condition.metric, op: metric.type === 'RATING' ? 'GT' : this.state.op, @@ -105,38 +118,30 @@ export default class Condition extends Component { } if (metric.key.indexOf('new_') === 0) { - data.period = '1'; + data.period = 1; } - e.preventDefault(); - updateCondition(data) - .then(newCondition => { - this.setState({ changed: false }); - onSaveCondition(condition, newCondition); - onResetError(); - }) - .catch(onError); + updateCondition({ organization, ...data }).then( + this.handleConditionResponse, + this.props.onError + ); }; - handleDeleteClick = e => { - const { qualityGate, condition, metric, onDeleteCondition } = this.props; - - e.preventDefault(); - new DeleteConditionView({ - qualityGate, - condition, - metric, - onDelete: () => onDeleteCondition(condition) - }).render(); + handleConditionResponse = (newCondition: ICondition) => { + this.setState({ changed: false }); + this.props.onSaveCondition(this.props.condition, newCondition); + this.props.onResetError(); }; - handleCancelClick = e => { - const { condition, onDeleteCondition } = this.props; - + handleCancelClick = (e: React.SyntheticEvent<HTMLAnchorElement>) => { e.preventDefault(); - onDeleteCondition(condition); + 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; @@ -175,7 +180,7 @@ export default class Condition extends Component { renderOperator() { const { condition, edit, metric } = this.props; - if (!edit) { + if (!edit && condition.op) { return metric.type === 'RATING' ? translate('quality_gates.operator', condition.op, 'rating') : translate('quality_gates.operator', condition.op); @@ -193,9 +198,9 @@ export default class Condition extends Component { return ( <Select + autofocus={true} className="input-medium" clearable={false} - innerRef={node => (this.operator = node)} name="operator" onChange={this.handleOperatorChange} options={operatorOptions} @@ -206,7 +211,7 @@ export default class Condition extends Component { } render() { - const { condition, edit, metric } = this.props; + const { condition, edit, metric, organization } = this.props; return ( <tr> <td className="text-middle"> @@ -258,9 +263,18 @@ export default class Condition extends Component { </button> <button className="button-red delete-condition little-spacer-left" - onClick={this.handleDeleteClick}> + 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> 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 index 7986f5a1816..547543cb215 100644 --- 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 @@ -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 index 00000000000..c4d1ea5fcfa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteConditionForm.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx index ab80d7e9df9..6bb533c415c 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx @@ -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)); 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 index b6ff64b5c87..a231079b6a2 100644 --- 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 @@ -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> ); 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 index 0489459a58c..8a424be367e 100644 --- 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 @@ -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> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx index aa3d836b237..f781cc253b8 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx @@ -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), () => {}); } }; 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 index 33a9c447d13..344bf1e2881 100644 --- 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 @@ -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(); } 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 index 96c3ed76f9a..0ec8d28794d 100644 --- 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 @@ -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.tsx index b00a7e8419f..cf60765a6f3 100644 --- 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.tsx @@ -17,23 +17,23 @@ * 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 * as React from 'react'; import Select from '../../../components/controls/Select'; +import { Metric } from '../../../app/types'; -export default class ThresholdInput extends React.PureComponent { - static propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.any, - metric: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired - }; +interface Props { + name: string; + value: string; + metric: Metric; + onChange: (value: string) => void; +} - handleChange = e => { - this.props.onChange(e.target.value); +export default class ThresholdInput extends React.PureComponent<Props> { + handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => { + this.props.onChange(e.currentTarget.value); }; - handleSelectChange = option => { + handleSelectChange = (option: any) => { if (option) { this.props.onChange(option.value); } else { @@ -51,17 +51,15 @@ export default class ThresholdInput extends React.PureComponent { { label: 'D', value: '4' } ]; - const realValue = value === '' ? null : value; - return ( <Select className="input-tiny text-middle" name={name} - value={realValue} + onChange={this.handleSelectChange} options={options} - searchable={false} placeholder="" - onChange={this.handleSelectChange} + searchable={false} + value={value} /> ); } @@ -80,7 +78,6 @@ export default class ThresholdInput extends React.PureComponent { 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/__tests__/ThresholdInput-test.js b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx index 32727393559..42f29a6a6a8 100644 --- 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.tsx @@ -17,15 +17,16 @@ * 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 * 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={{ type: 'INTEGER' }} onChange={jest.fn()} /> + <ThresholdInput name="foo" value="2" metric={metric} onChange={jest.fn()} /> ).find('input'); expect(input.length).toEqual(1); expect(input.prop('name')).toEqual('foo'); @@ -35,7 +36,7 @@ describe('on strings', () => { it('should change', () => { const onChange = jest.fn(); const input = shallow( - <ThresholdInput name="foo" value="2" metric={{ type: 'INTEGER' }} onChange={onChange} /> + <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} /> ).find('input'); change(input, 'bar'); expect(onChange).toBeCalledWith('bar'); @@ -43,9 +44,10 @@ describe('on strings', () => { }); describe('on ratings', () => { + const metric = { key: 'foo', name: 'Foo', type: 'RATING' }; it('should render Select', () => { const select = shallow( - <ThresholdInput name="foo" value="2" metric={{ type: 'RATING' }} onChange={jest.fn()} /> + <ThresholdInput name="foo" value="2" metric={metric} onChange={jest.fn()} /> ).find('Select'); expect(select.length).toEqual(1); expect(select.prop('value')).toEqual('2'); @@ -54,18 +56,18 @@ describe('on ratings', () => { it('should set', () => { const onChange = jest.fn(); const select = shallow( - <ThresholdInput name="foo" value="2" metric={{ type: 'RATING' }} onChange={onChange} /> + <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} /> ).find('Select'); - select.prop('onChange')({ label: 'D', value: '4' }); + (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={{ type: 'RATING' }} onChange={onChange} /> + <ThresholdInput name="foo" value="2" metric={metric} onChange={onChange} /> ).find('Select'); - select.prop('onChange')(null); + (select.prop('onChange') as Function)(null); expect(onChange).toBeCalledWith(''); }); }); |