diff options
Diffstat (limited to 'server/sonar-web/src/main/js')
24 files changed, 372 insertions, 247 deletions
diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts index 90b03cbe338..93fd2fd6e07 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -17,16 +17,19 @@ * 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); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js index 8ce26596388..cab5eacead8 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.js +++ b/server/sonar-web/src/main/js/apps/overview/components/App.js @@ -34,7 +34,8 @@ type Props = { id: string, key: string, qualifier: string, - tags: Array<string> + tags: Array<string>, + organization?: string }, onComponentChange: {} => void, router: Object diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js index 8149f8963f4..92a248cec4a 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js @@ -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({ diff --git a/server/sonar-web/src/main/js/apps/overview/types.js b/server/sonar-web/src/main/js/apps/overview/types.js index bc3c77aaea7..5a5cdb5df77 100644 --- a/server/sonar-web/src/main/js/apps/overview/types.js +++ b/server/sonar-web/src/main/js/apps/overview/types.js @@ -23,7 +23,8 @@ export type Component = { id: string, key: string, - qualifier: string + qualifier: string, + organization?: string }; */ diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx index 95b899a9aa3..5297b4ecbe4 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx @@ -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 }); diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx index 909767bfed9..8b762e76f19 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx index d04a0ee2f6e..2252afd252e 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx @@ -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; } 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(''); }); }); 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 index ad94c833969..e8b688273b2 100644 --- 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 @@ -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, 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 index 56078fc6518..b63d134e2ac 100644 --- 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 @@ -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 index 5ca1ac2944c..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/templates/quality-gates-condition-delete.hbs +++ /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 index f9f01acf285..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/views/gate-conditions-delete-view.js +++ /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) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/views/gate-projects-view.js b/server/sonar-web/src/main/js/apps/quality-gates/views/gate-projects-view.js index 1b54745a857..6c648020b99 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/views/gate-projects-view.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/views/gate-projects-view.js @@ -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, |