* 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 {
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: {
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: {
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);
}
id: string,
key: string,
qualifier: string,
- tags: Array<string>
+ tags: Array<string>,
+ organization?: string
},
onComponentChange: {} => void,
router: Object
/*::
type Props = {
- component: { key: string }
+ component: { key: string, organization?: string }
};
*/
}
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({
export type Component = {
id: string,
key: string,
- qualifier: string
+ qualifier: string,
+ organization?: string
};
*/
interface Props {
component: Component;
+ onComponentChange: (changes: {}) => void;
}
interface State {
}
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 });
);
}
- 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) {
const newGate = allGates.find(gate => gate.id === newId);
if (newGate) {
this.setState({ gate: newGate });
+ this.props.onComponentChange({ qualityGate: newGate });
}
} else {
this.setState({ gate: undefined });
interface Props {
allGates: QualityGate[];
gate?: QualityGate;
- onChange: (oldGate: number | undefined, newGate: number) => Promise<void>;
+ onChange: (oldGate?: number, newGate?: number) => Promise<void>;
}
interface State {
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', () => {
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) {
}
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;
}
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>
);
+++ /dev/null
-/*
- * 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>
- );
- }
-}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
edit,
onAddCondition,
onSaveCondition,
- onDeleteCondition
+ onDeleteCondition,
+ organization
} = this.props;
const existingConditions = conditions.filter(condition => metrics[condition.metric]);
onDeleteCondition={onDeleteCondition}
onError={this.handleError.bind(this)}
onResetError={this.handleResetError.bind(this)}
+ organization={organization}
/>
))}
</tbody>
--- /dev/null
+/*
+* 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>
+ );
+ }
+}
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));
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';
}
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;
onAddCondition={onAddCondition}
onSaveCondition={onSaveCondition}
onDeleteCondition={onDeleteCondition}
+ organization={organization && organization.key}
/>
</div>
);
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 || {};
onAddCondition={onAddCondition}
onSaveCondition={onSaveCondition}
onDeleteCondition={onDeleteCondition}
+ organization={organization}
/>
<div id="quality-gate-projects" className="quality-gate-section">
{gate.isDefault ? (
defaultMessage
) : (
- <Projects qualityGate={gate} edit={actions.associateProjects} />
+ <Projects
+ qualityGate={gate}
+ edit={actions.associateProjects}
+ organization={organization}
+ />
)}
</div>
</div>
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 {
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), () => {});
}
};
}
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();
}
}
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;
+++ /dev/null
-/*
- * 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}
- />
- );
- }
-}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+ }
+}
+++ /dev/null
-/*
- * 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('');
- });
-});
--- /dev/null
+/*
+ * 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('');
+ });
+});
};
}
-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,
return { ...candidate, isDefault: candidate.id === action.qualityGate.id };
}),
qualityGate: {
- ...state.qualityGate,
+ ...action.qualityGate,
isDefault: state.qualityGate.id === action.qualityGate.id
}
};
+++ /dev/null
-<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>
+++ /dev/null
-/*
- * 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)
- };
- }
-});
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({
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,