From: Grégoire Aubert Date: Tue, 8 May 2018 06:43:32 +0000 (+0200) Subject: SONAR-10513 Remove redux store form quality gates app X-Git-Tag: 7.5~1253 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c9b7be242b0cc8513923d6a38c4c957aa25dd170;p=sonarqube.git SONAR-10513 Remove redux store form quality gates app --- 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 fc72234b27e..490b48c22b1 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -19,35 +19,7 @@ */ import { getJSON, post, postJSON } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; -import { Metric } from '../app/types'; - -export interface ConditionBase { - error: string; - metric: string; - op?: string; - period?: number; - warning: string; -} - -export interface Condition extends ConditionBase { - id: number; -} - -export interface QualityGate { - actions?: { - associateProjects: boolean; - copy: boolean; - delete: boolean; - manageConditions: boolean; - rename: boolean; - setAsDefault: boolean; - }; - conditions?: Condition[]; - id: number; - isBuiltIn?: boolean; - isDefault?: boolean; - name: string; -} +import { Condition, Metric, QualityGate } from '../app/types'; export function fetchQualityGates(data: { organization?: string; @@ -106,7 +78,7 @@ export function createCondition( data: { gateId: number; organization?: string; - } & ConditionBase + } & Condition ): Promise { return postJSON('/api/qualitygates/create_condition', data); } @@ -136,10 +108,11 @@ export function getGateForProject(data: { export function searchGates(data: { gateId: number; organization?: string; - page: number; - pageSize: number; - selected: string; -}): Promise { + page?: number; + pageSize?: number; + query?: string; + selected?: string; +}): Promise<{ more: boolean; results: Array<{ id: string; name: string; selected: boolean }> }> { return getJSON('/api/qualitygates/search', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css index 0ae00301aa3..ac3fd543e5f 100644 --- a/server/sonar-web/src/main/js/app/styles/components/menu.css +++ b/server/sonar-web/src/main/js/app/styles/components/menu.css @@ -62,6 +62,10 @@ pointer-events: none !important; } +.menu > li > a.text-muted { + color: var(--secondFontColor); +} + .menu > li > a:hover, .menu > li > a:focus { text-decoration: none; diff --git a/server/sonar-web/src/main/js/app/styles/init/type.css b/server/sonar-web/src/main/js/app/styles/init/type.css index f0c235042ca..c150ffb4c7c 100644 --- a/server/sonar-web/src/main/js/app/styles/init/type.css +++ b/server/sonar-web/src/main/js/app/styles/init/type.css @@ -246,7 +246,7 @@ small, } .text-muted { - color: var(--secondFontColor) !important; + color: var(--secondFontColor); } .text-muted-2 { diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 7d32fffa2ba..ffaf58534f1 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -82,6 +82,15 @@ interface ComponentConfiguration { showUpdateKey?: boolean; } +export interface Condition { + error: string; + id?: number; + metric: string; + op?: string; + period?: number; + warning: string; +} + export interface CoveredFile { key: string; longName: string; @@ -365,6 +374,22 @@ export interface PullRequest { url?: string; } +export interface QualityGate { + actions?: { + associateProjects?: boolean; + copy?: boolean; + delete?: boolean; + manageConditions?: boolean; + rename?: boolean; + setAsDefault?: boolean; + }; + conditions?: Condition[]; + id: number; + isBuiltIn?: boolean; + isDefault?: boolean; + name: string; +} + export interface Rule { isTemplate?: boolean; key: string; 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 9d1d86e32dc..2ecc5f07a4f 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx @@ -25,13 +25,12 @@ import { fetchQualityGates, getGateForProject, associateGateWithProject, - dissociateGateWithProject, - QualityGate + dissociateGateWithProject } from '../../api/quality-gates'; import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; -import { Component } from '../../app/types'; +import { Component, QualityGate } from '../../app/types'; import { translate } from '../../helpers/l10n'; interface Props { @@ -128,7 +127,7 @@ export default class App extends React.PureComponent { const { allGates, gate, loading } = this.state; return ( -
+
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 3a826687096..1ac3be588b2 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { QualityGate } from '../../api/quality-gates'; import Select from '../../components/controls/Select'; import { translate } from '../../helpers/l10n'; +import { QualityGate } from '../../app/types'; interface Props { allGates: QualityGate[]; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js b/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js deleted file mode 100644 index 03d0975b57b..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { omitBy, map, sortBy } from 'lodash'; -import Select from '../../../components/controls/Select'; -import { translate, getLocalizedMetricName, getLocalizedMetricDomain } from '../../../helpers/l10n'; - -export default function AddConditionForm({ metrics, onSelect }) { - function handleChange(option) { - const metric = option.value; - - // e.target.value = ''; - onSelect(metric); - } - - const metricsToDisplay = omitBy(metrics, metric => metric.hidden); - const options = sortBy( - map(metricsToDisplay, metric => ({ - value: metric.key, - label: getLocalizedMetricName(metric), - domain: metric.domain - })), - 'domain' - ); - - // use "disabled" property to emulate optgroups - const optionsWithDomains = []; - options.forEach((option, index, options) => { - const previous = index > 0 ? options[index - 1] : null; - if (!previous || previous.domain !== option.domain) { - optionsWithDomains.push({ - value: option.domain, - label: getLocalizedMetricDomain(option.domain), - disabled: true - }); - } - optionsWithDomains.push(option); - }); - - return ( -
- +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx index 2fd57fb40b8..e12692f12e3 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx @@ -20,39 +20,33 @@ import * as React from 'react'; import DeleteConditionForm from './DeleteConditionForm'; import ThresholdInput from './ThresholdInput'; -import { - Condition as ICondition, - ConditionBase, - createCondition, - QualityGate, - updateCondition -} from '../../../api/quality-gates'; -import { Metric } from '../../../app/types'; +import { createCondition, updateCondition } from '../../../api/quality-gates'; +import { Condition as ICondition, Metric, QualityGate } from '../../../app/types'; import Checkbox from '../../../components/controls/Checkbox'; import Select from '../../../components/controls/Select'; import { Button, ResetButtonLink } from '../../../components/ui/buttons'; import { translate, getLocalizedMetricName } from '../../../helpers/l10n'; -import { formatMeasure } from '../../../helpers/measures'; +import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; interface Props { condition: ICondition; - edit: boolean; + canEdit: boolean; metric: Metric; - organization: string; - onDeleteCondition: (condition: ICondition) => void; + organization?: string; + onAddCondition: (metric: string) => void; onError: (error: any) => void; + onRemoveCondition: (Condition: ICondition) => void; onResetError: () => void; - onSaveCondition: (condition: ICondition, newCondition: ICondition) => void; + onSaveCondition: (newCondition: ICondition, oldCondition: ICondition) => void; qualityGate: QualityGate; } interface State { changed: boolean; - period?: number; + error: string; op?: string; - openDeleteCondition: boolean; + period?: number; warning: string; - error: string; } export default class Condition extends React.PureComponent { @@ -62,41 +56,45 @@ export default class Condition extends React.PureComponent { changed: false, period: props.condition.period, op: props.condition.op, - openDeleteCondition: false, warning: props.condition.warning || '', error: props.condition.error || '' }; } - handleOperatorChange = ({ value }: any) => this.setState({ changed: true, op: value }); - - handlePeriodChange = (checked: boolean) => { - const period = checked ? 1 : undefined; - this.setState({ changed: true, period }); - }; - - handleWarningChange = (warning: string) => this.setState({ changed: true, warning }); - - handleErrorChange = (error: string) => this.setState({ changed: true, error }); - - handleSaveClick = () => { - const { qualityGate, condition, metric, organization } = this.props; - const { period } = this.state; - const data: ConditionBase = { - metric: condition.metric, + getUpdatedCondition = () => { + const { metric } = this.props; + const data: ICondition = { + metric: metric.key, op: metric.type === 'RATING' ? 'GT' : this.state.op, warning: this.state.warning, error: this.state.error }; + const { period } = this.state; if (period && metric.type !== 'RATING') { data.period = period; } - if (metric.key.indexOf('new_') === 0) { + if (isDiffMetric(metric.key)) { data.period = 1; } + return data; + }; + + handleOperatorChange = ({ value }: any) => this.setState({ changed: true, op: value }); + + handlePeriodChange = (checked: boolean) => { + const period = checked ? 1 : undefined; + this.setState({ changed: true, period }); + }; + + handleWarningChange = (warning: string) => this.setState({ changed: true, warning }); + handleErrorChange = (error: string) => this.setState({ changed: true, error }); + + handleSaveClick = () => { + const { qualityGate, organization } = this.props; + const data = this.getUpdatedCondition(); createCondition({ gateId: qualityGate.id, organization, ...data }).then( this.handleConditionResponse, this.props.onError @@ -104,24 +102,12 @@ export default class Condition extends React.PureComponent { }; handleUpdateClick = () => { - const { condition, metric, organization } = this.props; - const { period } = this.state; + const { condition, organization } = this.props; const data: ICondition = { id: condition.id, - metric: condition.metric, - op: metric.type === 'RATING' ? 'GT' : this.state.op, - warning: this.state.warning, - error: this.state.error + ...this.getUpdatedCondition() }; - if (period && metric.type !== 'RATING') { - data.period = period; - } - - if (metric.key.indexOf('new_') === 0) { - data.period = 1; - } - updateCondition({ organization, ...data }).then( this.handleConditionResponse, this.props.onError @@ -129,30 +115,21 @@ export default class Condition extends React.PureComponent { }; handleConditionResponse = (newCondition: ICondition) => { - this.setState({ changed: false }); - this.props.onSaveCondition(this.props.condition, newCondition); + this.props.onSaveCondition(newCondition, this.props.condition); this.props.onResetError(); + this.setState({ changed: false }); }; handleCancelClick = () => { - this.props.onDeleteCondition(this.props.condition); - }; - - openDeleteConditionForm = () => { - this.setState({ openDeleteCondition: true }); - }; - - closeDeleteConditionForm = () => { - this.setState({ openDeleteCondition: false }); + this.props.onRemoveCondition(this.props.condition); }; renderPeriodValue() { const { condition, metric } = this.props; const isLeakSelected = !!this.state.period; - const isDiffMetric = condition.metric.indexOf('new_') === 0; const isRating = metric.type === 'RATING'; - if (isDiffMetric) { + if (isDiffMetric(condition.metric)) { return ( {translate('quality_gates.condition.leak.unconditional')} ); @@ -168,13 +145,11 @@ export default class Condition extends React.PureComponent { } renderPeriod() { - const { condition, metric, edit } = this.props; - - const isDiffMetric = condition.metric.indexOf('new_') === 0; + const { condition, metric, canEdit } = this.props; const isRating = metric.type === 'RATING'; const isLeakSelected = !!this.state.period; - if (isRating || isDiffMetric || !edit) { + if (isRating || isDiffMetric(condition.metric) || !canEdit) { return this.renderPeriodValue(); } @@ -182,9 +157,9 @@ export default class Condition extends React.PureComponent { } renderOperator() { - const { condition, edit, metric } = this.props; + const { condition, canEdit, metric } = this.props; - if (!edit && condition.op) { + if (!canEdit && condition.op) { return metric.type === 'RATING' ? translate('quality_gates.operator', condition.op, 'rating') : translate('quality_gates.operator', condition.op); @@ -215,7 +190,7 @@ export default class Condition extends React.PureComponent { } render() { - const { condition, edit, metric, organization } = this.props; + const { condition, canEdit, metric, organization } = this.props; return ( @@ -230,7 +205,7 @@ export default class Condition extends React.PureComponent { {this.renderOperator()} - {edit ? ( + {canEdit ? ( { - {edit ? ( + {canEdit ? ( { )} - {edit && ( + {canEdit && ( {condition.id ? (
@@ -265,20 +240,12 @@ export default class Condition extends React.PureComponent { onClick={this.handleUpdateClick}> {translate('update_verb')} - - {this.state.openDeleteCondition && ( - - )} +
) : (
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js deleted file mode 100644 index 982d6d9f2ab..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { sortBy, uniqBy } from 'lodash'; -import AddConditionForm from './AddConditionForm'; -import Condition from './Condition'; -import DocTooltip from '../../../components/docs/DocTooltip'; -import { translate, getLocalizedMetricName } from '../../../helpers/l10n'; - -function getKey(condition, index) { - return condition.id ? condition.id : `new-${index}`; -} - -export default class Conditions extends React.PureComponent { - state = { - error: null - }; - - componentWillUpdate(nextProps) { - if (nextProps.qualityGate !== this.props.qualityGate) { - this.setState({ error: null }); - } - } - - handleError(error) { - try { - error.response.json().then(r => { - const message = r.errors.map(e => e.msg).join('. '); - this.setState({ error: message }); - }); - } catch (ex) { - this.setState({ error: translate('default_error_message') }); - } - } - - handleResetError() { - this.setState({ error: null }); - } - - render() { - const { - qualityGate, - conditions, - metrics, - edit, - onAddCondition, - onSaveCondition, - onDeleteCondition, - organization - } = this.props; - - const existingConditions = conditions.filter(condition => metrics[condition.metric]); - - const sortedConditions = sortBy( - existingConditions, - condition => metrics[condition.metric] && metrics[condition.metric].name - ); - - const duplicates = []; - const savedConditions = existingConditions.filter(condition => condition.id != null); - savedConditions.forEach(condition => { - const sameCount = savedConditions.filter( - sample => sample.metric === condition.metric && sample.period === condition.period - ).length; - if (sameCount > 1) { - duplicates.push(condition); - } - }); - - const uniqDuplicates = uniqBy(duplicates, d => d.metric).map(condition => ({ - ...condition, - metric: metrics[condition.metric] - })); - return ( -
-
-

{translate('quality_gates.conditions')}

- -
- -
{translate('quality_gates.introduction')}
- - {this.state.error &&
{this.state.error}
} - - {uniqDuplicates.length > 0 && ( -
-

{translate('quality_gates.duplicated_conditions')}

-
    - {uniqDuplicates.map(d => ( -
  • {getLocalizedMetricName(d.metric)}
  • - ))} -
-
- )} - - {sortedConditions.length ? ( - - - - - - - - - {edit && - - - {sortedConditions.map((condition, index) => ( - - ))} - -
{translate('quality_gates.conditions.metric')}{translate('quality_gates.conditions.leak')}{translate('quality_gates.conditions.operator')}{translate('quality_gates.conditions.warning')}{translate('quality_gates.conditions.error')}} -
- ) : ( -
{translate('quality_gates.no_conditions')}
- )} - - {edit && } -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx new file mode 100644 index 00000000000..0df36b5110f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -0,0 +1,169 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { differenceWith, map, sortBy, uniqBy } from 'lodash'; +import AddConditionSelect from './AddConditionSelect'; +import Condition from './Condition'; +import DocTooltip from '../../../components/docs/DocTooltip'; +import { translate, getLocalizedMetricName } from '../../../helpers/l10n'; +import { Condition as ICondition, Metric, QualityGate } from '../../../app/types'; +import { parseError } from '../../../helpers/request'; + +interface Props { + canEdit: boolean; + conditions: ICondition[]; + metrics: { [key: string]: Metric }; + onAddCondition: (metric: string) => void; + onSaveCondition: (newCondition: ICondition, oldCondition: ICondition) => void; + onRemoveCondition: (Condition: ICondition) => void; + organization?: string; + qualityGate: QualityGate; +} + +interface State { + error?: string; +} + +export default class Conditions extends React.PureComponent { + state: State = {}; + + componentWillUpdate(nextProps: Props) { + if (nextProps.qualityGate !== this.props.qualityGate) { + this.setState({ error: undefined }); + } + } + + getConditionKey = (condition: ICondition, index: number) => { + return condition.id ? condition.id : `new-${index}`; + }; + + handleError = (error: any) => { + parseError(error).then( + message => { + this.setState({ error: message }); + }, + () => {} + ); + }; + + handleResetError = () => { + this.setState({ error: undefined }); + }; + + render() { + const { qualityGate, conditions, metrics, canEdit, organization } = this.props; + + const existingConditions = conditions.filter(condition => metrics[condition.metric]); + + const sortedConditions = sortBy( + existingConditions, + condition => metrics[condition.metric] && metrics[condition.metric].name + ); + + const duplicates: ICondition[] = []; + const savedConditions = existingConditions.filter(condition => condition.id != null); + savedConditions.forEach(condition => { + const sameCount = savedConditions.filter( + sample => sample.metric === condition.metric && sample.period === condition.period + ).length; + if (sameCount > 1) { + duplicates.push(condition); + } + }); + + const uniqDuplicates = uniqBy(duplicates, d => d.metric).map(condition => ({ + ...condition, + metric: metrics[condition.metric] + })); + + const availableMetrics = differenceWith( + map(metrics, metric => metric).filter( + metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type) + ), + conditions, + (metric, condition) => metric.key === condition.metric + ); + + return ( +
+
+

{translate('quality_gates.conditions')}

+ +
+ +
{translate('quality_gates.introduction')}
+ + {this.state.error &&
{this.state.error}
} + + {uniqDuplicates.length > 0 && ( +
+

{translate('quality_gates.duplicated_conditions')}

+
    + {uniqDuplicates.map(d => ( +
  • {getLocalizedMetricName(d.metric)}
  • + ))} +
+
+ )} + + {sortedConditions.length ? ( + + + + + + + + + {canEdit && + + + {sortedConditions.map((condition, index) => ( + + ))} + +
{translate('quality_gates.conditions.metric')}{translate('quality_gates.conditions.leak')}{translate('quality_gates.conditions.operator')}{translate('quality_gates.conditions.warning')}{translate('quality_gates.conditions.error')}} +
+ ) : ( +
{translate('quality_gates.no_conditions')}
+ )} + + {canEdit && ( + + )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx index 7583b76de78..656dd635ee4 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx @@ -19,112 +19,86 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { copyQualityGate, QualityGate } from '../../../api/quality-gates'; -import Modal from '../../../components/controls/Modal'; -import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons'; +import { copyQualityGate } from '../../../api/quality-gates'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; +import { QualityGate } from '../../../app/types'; interface Props { - qualityGate: QualityGate; - onCopy: (newQualityGate: QualityGate) => void; - onClose: () => void; + onCopy: () => Promise; organization?: string; + qualityGate: QualityGate; } interface State { - loading: boolean; name: string; } export default class CopyQualityGateForm extends React.PureComponent { - mounted = false; - static contextTypes = { router: PropTypes.object }; constructor(props: Props) { super(props); - this.state = { loading: false, name: props.qualityGate.name }; - } - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; + this.state = { name: props.qualityGate.name }; } - handleNameChange = (event: React.SyntheticEvent) => { + handleNameChange = (event: React.ChangeEvent) => { this.setState({ name: event.currentTarget.value }); }; - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); + onCopy = () => { const { qualityGate, organization } = this.props; const { name } = this.state; - if (name) { - this.setState({ loading: true }); - copyQualityGate({ id: qualityGate.id, name, organization }).then( - qualityGate => { - this.props.onCopy(qualityGate); - this.props.onClose(); - this.context.router.push( - getQualityGateUrl(String(qualityGate.id), this.props.organization) - ); - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); + + if (!name) { + return undefined; } + + return copyQualityGate({ id: qualityGate.id, name, organization }).then(qualityGate => { + this.props.onCopy(); + this.context.router.push(getQualityGateUrl(String(qualityGate.id), this.props.organization)); + }); }; render() { const { qualityGate } = this.props; - const { loading, name } = this.state; - const header = translate('quality_gates.copy'); - const submitDisabled = loading || !name || (qualityGate && qualityGate.name === name); + const { name } = this.state; + const confirmDisable = !name || (qualityGate && qualityGate.name === name); return ( - -
-
-

{header}

+ + +
-
-
- - -
-
-
- {loading && } - - {translate('copy')} - - - {translate('cancel')} - -
- -
+ } + modalHeader={translate('quality_gates.copy')} + onConfirm={this.onCopy}> + {({ onClick }) => ( + + )} + ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx index 6a0ca705301..b984d962941 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx @@ -19,105 +19,81 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { createQualityGate, QualityGate } from '../../../api/quality-gates'; -import Modal from '../../../components/controls/Modal'; -import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { createQualityGate } from '../../../api/quality-gates'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; interface Props { - onCreate: (qualityGate: QualityGate) => void; - onClose: () => void; + onCreate: () => Promise; organization?: string; } interface State { - loading: boolean; name: string; } export default class CreateQualityGateForm extends React.PureComponent { - mounted = false; - static contextTypes = { router: PropTypes.object }; - state = { loading: false, name: '' }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } + state = { name: '' }; handleNameChange = (event: React.SyntheticEvent) => { this.setState({ name: event.currentTarget.value }); }; - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); + handleCreate = () => { const { organization } = this.props; const { name } = this.state; - if (name) { - this.setState({ loading: true }); - createQualityGate({ name, organization }).then( - qualityGate => { - this.props.onCreate(qualityGate); - this.context.router.push(getQualityGateUrl(String(qualityGate.id), organization)); - this.props.onClose(); - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); + + if (!name) { + return undefined; } + + return createQualityGate({ name, organization }) + .then(qualityGate => { + return this.props.onCreate().then(() => qualityGate); + }) + .then(qualityGate => { + this.context.router.push(getQualityGateUrl(String(qualityGate.id), organization)); + }); }; render() { - const { loading, name } = this.state; - const header = translate('quality_gates.create'); - const submitDisabled = loading || !name; - + const { name } = this.state; return ( - -
-
-

{header}

+ + +
-
-
- - -
-
-
- {loading && } - - {translate('save')} - - - {translate('cancel')} - -
- -
+ } + modalHeader={translate('quality_gates.create')} + onConfirm={this.handleCreate}> + {({ onClick }) => ( + + )} + ); } } 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 index ce41277c1ad..3b52e3a8f5f 100644 --- 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 @@ -18,79 +18,47 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Condition, deleteCondition } from '../../../api/quality-gates'; -import { Metric } from '../../../app/types'; -import Modal from '../../../components/controls/Modal'; -import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { deleteCondition } from '../../../api/quality-gates'; +import { Metric, Condition } from '../../../app/types'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; +import { Button } from '../../../components/ui/buttons'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; interface Props { condition: Condition; metric: Metric; - onClose: () => void; onDelete: (condition: Condition) => void; organization?: string; } -interface State { - loading: boolean; -} - -export default class DeleteConditionForm extends React.PureComponent { - mounted = false; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); +export default class DeleteConditionForm extends React.PureComponent { + onDelete = () => { const { organization, condition } = this.props; - this.setState({ loading: true }); - deleteCondition({ id: condition.id, organization }).then( - () => this.props.onDelete(condition), - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); + if (condition.id !== undefined) { + return deleteCondition({ id: condition.id, organization }).then(() => + this.props.onDelete(condition) + ); + } + return undefined; }; render() { - const { metric } = this.props; - const header = translate('quality_gates.delete_condition'); - return ( - -
-
-

{header}

-
-
-

- {translateWithParameters( - 'quality_gates.delete_condition.confirm.message', - getLocalizedMetricName(metric) - )} -

-
-
- {this.state.loading && } - - {translate('delete')} - - - {translate('cancel')} - -
- -
+ + {({ onClick }) => ( + + )} + ); } } 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 1d0edec416a..818b2d6d052 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 @@ -19,83 +19,55 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { deleteQualityGate, QualityGate } from '../../../api/quality-gates'; -import Modal from '../../../components/controls/Modal'; -import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import { deleteQualityGate } from '../../../api/quality-gates'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; +import { Button } from '../../../components/ui/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityGatesUrl } from '../../../helpers/urls'; +import { QualityGate } from '../../../app/types'; interface Props { - onClose: () => void; - onDelete: (qualityGate: QualityGate) => void; + onDelete: () => Promise; organization?: string; qualityGate: QualityGate; } -interface State { - loading: boolean; -} - -export default class DeleteQualityGateForm extends React.PureComponent { - mounted = false; - +export default class DeleteQualityGateForm extends React.PureComponent { static contextTypes = { router: PropTypes.object }; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); + onDelete = () => { const { organization, qualityGate } = this.props; - this.setState({ loading: true }); - deleteQualityGate({ id: qualityGate.id, organization }).then( - () => { - this.props.onDelete(qualityGate); - this.context.router.replace(getQualityGatesUrl(organization)); - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); + return deleteQualityGate({ id: qualityGate.id, organization }) + .then(this.props.onDelete) + .then(() => { + this.context.router.push(getQualityGatesUrl(organization)); + }); }; render() { const { qualityGate } = this.props; - const header = translate('quality_gates.delete'); return ( - -
-
-

{header}

-
-
-

- {translateWithParameters('quality_gates.delete.confirm.message', qualityGate.name)} -

-
-
- {this.state.loading && } - - {translate('delete')} - - - {translate('cancel')} - -
- -
+ + {({ onClick }) => ( + + )} + ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js deleted file mode 100644 index 37c9627a8df..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import Helmet from 'react-helmet'; -import DetailsHeader from './DetailsHeader'; -import DetailsContent from './DetailsContent'; -import { fetchQualityGate } from '../../../api/quality-gates'; - -export default class Details extends React.PureComponent { - static contextTypes = { - router: PropTypes.object.isRequired - }; - - componentDidMount() { - this.props.fetchMetrics(); - this.fetchDetails(); - } - - componentDidUpdate(prevProps) { - if (prevProps.params.id !== this.props.params.id) { - this.fetchDetails(); - } - } - - fetchDetails = () => - fetchQualityGate({ - id: this.props.params.id, - organization: this.props.organization && this.props.organization.key - }).then(qualityGate => this.props.onShow(qualityGate), () => {}); - - render() { - const { organization, metrics, qualityGate } = this.props; - const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props; - - if (!qualityGate) { - return null; - } - - return ( -
- - - - -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx new file mode 100644 index 00000000000..4c01648d41c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx @@ -0,0 +1,180 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import Helmet from 'react-helmet'; +import { connect } from 'react-redux'; +import DetailsHeader from './DetailsHeader'; +import DetailsContent from './DetailsContent'; +import { getMetrics } from '../../../store/rootReducer'; +import { fetchMetrics } from '../../../store/rootActions'; +import { fetchQualityGate } from '../../../api/quality-gates'; +import { Metric, QualityGate, Condition } from '../../../app/types'; +import { checkIfDefault, addCondition, replaceCondition, deleteCondition } from '../utils'; + +interface OwnProps { + onSetDefault: (qualityGate: QualityGate) => void; + organization?: string; + params: { id: number }; + qualityGates: QualityGate[]; + refreshQualityGates: () => Promise; +} + +interface StateToProps { + metrics: { [key: string]: Metric }; +} + +interface DispatchToProps { + fetchMetrics: () => void; +} + +type Props = StateToProps & DispatchToProps & OwnProps; + +interface State { + loading: boolean; + qualityGate?: QualityGate; +} + +export class DetailsApp extends React.PureComponent { + mounted = false; + + static contextTypes = { + router: PropTypes.object.isRequired + }; + + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + this.props.fetchMetrics(); + this.fetchDetails(); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.params.id !== this.props.params.id) { + this.setState({ loading: true }); + this.fetchDetails(nextProps); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchDetails = ({ organization, params } = this.props) => { + return fetchQualityGate({ id: params.id, organization }).then( + qualityGate => { + if (this.mounted) { + this.setState({ loading: false, qualityGate }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleAddCondition = (metric: string) => { + this.setState(({ qualityGate }) => { + if (!qualityGate) { + return undefined; + } + return { qualityGate: addCondition(qualityGate, metric) }; + }); + }; + + handleSaveCondition = (newCondition: Condition, oldCondition: Condition) => { + this.setState(({ qualityGate }) => { + if (!qualityGate) { + return undefined; + } + return { qualityGate: replaceCondition(qualityGate, newCondition, oldCondition) }; + }); + }; + + handleRemoveCondition = (condition: Condition) => { + this.setState(({ qualityGate }) => { + if (!qualityGate) { + return undefined; + } + return { qualityGate: deleteCondition(qualityGate, condition) }; + }); + }; + + handleSetDefault = () => { + this.setState(({ qualityGate }) => { + if (!qualityGate) { + return undefined; + } + this.props.onSetDefault(qualityGate); + const newQualityGate: QualityGate = { + ...qualityGate, + actions: { ...qualityGate.actions, delete: false, setAsDefault: false } + }; + return { qualityGate: newQualityGate }; + }); + }; + + render() { + const { organization, metrics, refreshQualityGates } = this.props; + const { qualityGate } = this.state; + + if (!qualityGate) { + return null; + } + + return ( + <> + +
+ + +
+ + ); + } +} + +const mapDispatchToProps: DispatchToProps = { fetchMetrics }; + +const mapStateToProps = (state: any): StateToProps => ({ + metrics: getMetrics(state) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DetailsApp); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js deleted file mode 100644 index 35b37f12bae..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import Conditions from './Conditions'; -import Projects from './Projects'; -import DocTooltip from '../../../components/docs/DocTooltip'; -import { translate } from '../../../helpers/l10n'; - -export default class DetailsContent extends React.PureComponent { - render() { - const { gate, metrics, organization } = this.props; - const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props; - const conditions = gate.conditions || []; - const actions = gate.actions || {}; - - const defaultMessage = actions.associateProjects - ? translate('quality_gates.projects_for_default.edit') - : translate('quality_gates.projects_for_default'); - - return ( -
- - -
-
-

{translate('quality_gates.projects')}

- -
- {gate.isDefault ? ( - defaultMessage - ) : ( - - )} -
-
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx new file mode 100644 index 00000000000..7463d772635 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import Conditions from './Conditions'; +import Projects from './Projects'; +import DocTooltip from '../../../components/docs/DocTooltip'; +import { translate } from '../../../helpers/l10n'; +import { Condition as ICondition, Metric, QualityGate } from '../../../app/types'; + +interface Props { + isDefault?: boolean; + metrics: { [key: string]: Metric }; + organization?: string; + onAddCondition: (metric: string) => void; + onRemoveCondition: (Condition: ICondition) => void; + onSaveCondition: (newCondition: ICondition, oldCondition: ICondition) => void; + qualityGate: QualityGate; +} + +export default class DetailsContent extends React.PureComponent { + render() { + const { isDefault, metrics, organization, qualityGate } = this.props; + const conditions = qualityGate.conditions || []; + const actions = qualityGate.actions || ({} as any); + + return ( +
+ + +
+
+

{translate('quality_gates.projects')}

+ +
+ {isDefault ? ( + translate('quality_gates.projects_for_default') + ) : ( + + )} +
+
+ ); + } +} 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 c4c869924a3..b87ceed7ad2 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,52 +22,39 @@ import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; import RenameQualityGateForm from './RenameQualityGateForm'; import CopyQualityGateForm from './CopyQualityGateForm'; import DeleteQualityGateForm from './DeleteQualityGateForm'; -import { fetchQualityGate, QualityGate, setQualityGateAsDefault } from '../../../api/quality-gates'; +import { setQualityGateAsDefault } from '../../../api/quality-gates'; import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; +import { QualityGate } from '../../../app/types'; interface Props { - qualityGate: QualityGate; - onRename: (qualityGate: QualityGate, newName: string) => void; - onCopy: (newQualityGate: QualityGate) => void; - onSetAsDefault: (qualityGate: QualityGate) => void; - onDelete: (qualityGate: QualityGate) => void; organization?: string; + qualityGate: QualityGate; + onSetDefault: () => void; + refreshItem: () => Promise; + refreshList: () => Promise; } -interface State { - openPopup?: string; -} - -export default class DetailsHeader extends React.PureComponent { - state = { openPopup: undefined }; - - handleRenameClick = () => { - this.setState({ openPopup: 'rename' }); - }; - - handleCopyClick = () => { - this.setState({ openPopup: 'copy' }); +export default class DetailsHeader extends React.PureComponent { + handleActionRefresh = () => { + const { refreshItem, refreshList } = this.props; + return Promise.all([refreshItem(), refreshList()]).then(() => {}, () => {}); }; handleSetAsDefaultClick = () => { - const { qualityGate, onSetAsDefault, organization } = this.props; + const { organization, qualityGate } = this.props; if (!qualityGate.isDefault) { - setQualityGateAsDefault({ id: qualityGate.id, organization }) - .then(() => fetchQualityGate({ id: qualityGate.id, organization })) - .then(qualityGate => onSetAsDefault(qualityGate), () => {}); + // Optimistic update + this.props.onSetDefault(); + setQualityGateAsDefault({ id: qualityGate.id, organization }).then( + this.handleActionRefresh, + this.handleActionRefresh + ); } }; - handleDeleteClick = () => { - this.setState({ openPopup: 'delete' }); - }; - - handleClosePopup = () => this.setState({ openPopup: undefined }); - render() { const { organization, qualityGate } = this.props; - const { openPopup } = this.state; const actions = qualityGate.actions || ({} as any); return (
@@ -80,17 +67,18 @@ export default class DetailsHeader extends React.PureComponent {
{actions.rename && ( - + )} {actions.copy && ( - + )} {actions.setAsDefault && ( )} {actions.delete && ( - - )} - {openPopup === 'rename' && ( - - )} - - {openPopup === 'copy' && ( - - )} - - {openPopup === 'delete' && ( diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.js deleted file mode 100644 index 37fb224b0f8..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { translate } from '../../../helpers/l10n'; - -export default function Intro() { - return ( -
-
-
-

{translate('quality_gates.intro.1')}

-

{translate('quality_gates.intro.2')}

-
-
-
- ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.tsx new file mode 100644 index 00000000000..ea5edc9180c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Intro.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default function Intro() { + return ( +
+
+
+

{translate('quality_gates.intro.1')}

+

{translate('quality_gates.intro.2')}

+
+
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.js b/server/sonar-web/src/main/js/apps/quality-gates/components/List.js deleted file mode 100644 index a1319df5af5..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/List.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { Link } from 'react-router'; -import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; -import { translate } from '../../../helpers/l10n'; -import { getQualityGateUrl } from '../../../helpers/urls'; - -export default function List({ organization, qualityGates }) { - return ( -
- {qualityGates.map(qualityGate => ( - - - - - - - - -
{qualityGate.name} - {qualityGate.isDefault && {translate('default')}} - {qualityGate.isBuiltIn && ( - - )} -
- - ))} -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx new file mode 100644 index 00000000000..5586c52f22f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Link } from 'react-router'; +import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; +import { translate } from '../../../helpers/l10n'; +import { getQualityGateUrl } from '../../../helpers/urls'; +import { QualityGate } from '../../../app/types'; + +interface Props { + organization?: string; + qualityGates: QualityGate[]; +} + +export default function List({ organization, qualityGates }: Props) { + return ( +
+ {qualityGates.map(qualityGate => ( + + + + + + + + +
{qualityGate.name} + {qualityGate.isDefault && {translate('default')}} + {qualityGate.isBuiltIn && ( + + )} +
+ + ))} +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx index da61b1c5623..7ff64a81d9e 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx @@ -19,57 +19,28 @@ */ import * as React from 'react'; import CreateQualityGateForm from '../components/CreateQualityGateForm'; -import { QualityGate } from '../../../api/quality-gates'; -import { Organization } from '../../../app/types'; import DocTooltip from '../../../components/docs/DocTooltip'; -import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; interface Props { canCreate: boolean; - onAdd: (qualityGate: QualityGate) => void; - organization?: Organization; + refreshQualityGates: () => Promise; + organization?: string; } -interface State { - createQualityGateOpen: boolean; -} - -export default class ListHeader extends React.PureComponent { - state = { createQualityGateOpen: false }; - - openCreateQualityGateForm = () => { - this.setState({ createQualityGateOpen: true }); - }; - - closeCreateQualityGateForm = () => { - this.setState({ createQualityGateOpen: false }); - }; - - render() { - const { organization } = this.props; - - return ( -
- {this.props.canCreate && ( -
- -
- )} -
-

{translate('quality_gates.page')}

- +export default function ListHeader({ canCreate, refreshQualityGates, organization }: Props) { + return ( +
+ {canCreate && ( +
+
- {this.state.createQualityGateOpen && ( - - )} -
- ); - } + )} + +
+

{translate('quality_gates.page')}

+ +
+
+ ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js deleted file mode 100644 index b51bed9bd11..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { find, without } from 'lodash'; -import SelectList, { Filter } from '../../../components/SelectList/SelectList'; -import { translate } from '../../../helpers/l10n'; -import { - searchGates, - associateGateWithProject, - dissociateGateWithProject -} from '../../../api/quality-gates'; -/*:: import { Project } from '../../projects/types'; */ - -/*:: -type State = { - projects: Projects[], - selectedProjects: string[] -}; -*/ - -export default class Projects extends React.PureComponent { - state /*: State */ = { projects: [], selectedProjects: [] }; - - componentDidMount() { - this.handleSearch('', Filter.Selected); - } - - handleSearch = (query /*: string*/, selected /*: string */) => { - return searchGates({ - gateId: this.props.qualityGate.id, - organization: this.props.organization, - pageSize: 100, - query: query !== '' ? query : undefined, - selected - }).then(data => { - this.setState({ - projects: data.results, - selectedProjects: data.results - .filter(project => project.selected) - .map(project => project.id) - }); - }); - }; - - handleSelect = (id /*: string*/) => { - return associateGateWithProject({ - gateId: this.props.qualityGate.id, - organization: this.props.organization, - projectId: id - }).then(() => { - this.setState((state /*: State*/) => ({ - selectedProjects: [...state.selectedProjects, id] - })); - }); - }; - - handleUnselect = (id /*: string*/) => { - return dissociateGateWithProject({ - gateId: this.props.qualityGate.id, - organization: this.props.organization, - projectId: id - }).then( - () => { - this.setState((state /*: State*/) => ({ - selectedProjects: without(state.selectedProjects, id) - })); - }, - () => {} - ); - }; - - renderElement = (id /*: string*/) /*: React.ReactNode*/ => { - const project = find(this.state.projects, { id }); - return project === undefined ? id : project.name; - }; - - render() { - return ( - project.id)} - labelAll={translate('quality_gates.projects.all')} - labelSelected={translate('quality_gates.projects.with')} - labelUnselected={translate('quality_gates.projects.without')} - onSearch={this.handleSearch} - onSelect={this.handleSelect} - onUnselect={this.handleUnselect} - renderElement={this.renderElement} - selectedElements={this.state.selectedProjects} - /> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx new file mode 100644 index 00000000000..8153ec3e3ca --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx @@ -0,0 +1,114 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { find, without } from 'lodash'; +import SelectList, { Filter } from '../../../components/SelectList/SelectList'; +import { translate } from '../../../helpers/l10n'; +import { + searchGates, + associateGateWithProject, + dissociateGateWithProject +} from '../../../api/quality-gates'; +import { QualityGate } from '../../../app/types'; + +interface Props { + canEdit?: boolean; + organization?: string; + qualityGate: QualityGate; +} + +interface State { + projects: Array<{ id: string; name: string; selected: boolean }>; + selectedProjects: string[]; +} + +export default class Projects extends React.PureComponent { + state: State = { projects: [], selectedProjects: [] }; + + componentDidMount() { + this.handleSearch('', Filter.Selected); + } + + handleSearch = (query: string, selected: string) => { + return searchGates({ + gateId: this.props.qualityGate.id, + organization: this.props.organization, + pageSize: 100, + query: query !== '' ? query : undefined, + selected + }).then(data => { + this.setState({ + projects: data.results, + selectedProjects: data.results + .filter(project => project.selected) + .map(project => project.id) + }); + }); + }; + + handleSelect = (id: string) => { + return associateGateWithProject({ + gateId: this.props.qualityGate.id, + organization: this.props.organization, + projectId: id + }).then(() => { + this.setState(state => ({ + selectedProjects: [...state.selectedProjects, id] + })); + }); + }; + + handleUnselect = (id: string) => { + return dissociateGateWithProject({ + gateId: this.props.qualityGate.id, + organization: this.props.organization, + projectId: id + }).then( + () => { + this.setState(state => ({ + selectedProjects: without(state.selectedProjects, id) + })); + }, + () => {} + ); + }; + + renderElement = (id: string): React.ReactNode => { + const project = find(this.state.projects, { id }); + return project === undefined ? id : project.name; + }; + + render() { + return ( + project.id)} + labelAll={translate('quality_gates.projects.all')} + labelSelected={translate('quality_gates.projects.with')} + labelUnselected={translate('quality_gates.projects.without')} + onSearch={this.handleSearch} + onSelect={this.handleSelect} + onUnselect={this.handleUnselect} + readOnly={!this.props.canEdit} + renderElement={this.renderElement} + selectedElements={this.state.selectedProjects} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js deleted file mode 100644 index dbd4a441f63..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import Helmet from 'react-helmet'; -import ListHeader from './ListHeader'; -import List from './List'; -import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; -import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; -import { fetchQualityGates } from '../../../api/quality-gates'; -import { translate } from '../../../helpers/l10n'; -import { getQualityGateUrl } from '../../../helpers/urls'; -import '../styles.css'; - -export default class QualityGatesApp extends Component { - static contextTypes = { - router: PropTypes.object.isRequired - }; - - state = {}; - - componentDidMount() { - this.fetchQualityGates(); - // $FlowFixMe - document.body.classList.add('white-page'); - // $FlowFixMe - document.documentElement.classList.add('white-page'); - const footer = document.getElementById('footer'); - if (footer) { - footer.classList.add('page-footer-with-sidebar'); - } - } - - componentWillUnmount() { - // $FlowFixMe - document.body.classList.remove('white-page'); - // $FlowFixMe - document.documentElement.classList.remove('white-page'); - const footer = document.getElementById('footer'); - if (footer) { - footer.classList.remove('page-footer-with-sidebar'); - } - } - - fetchQualityGates = () => - fetchQualityGates({ - organization: this.props.organization && this.props.organization.key - }).then( - ({ actions, qualitygates: qualityGates }) => { - const { organization, updateStore } = this.props; - updateStore({ actions, qualityGates }); - if (qualityGates && qualityGates.length === 1 && !actions.create) { - this.context.router.replace( - getQualityGateUrl(String(qualityGates[0].id), organization && organization.key) - ); - } - }, - () => {} - ); - - render() { - const { children, qualityGates, actions, organization } = this.props; - const defaultTitle = translate('quality_gates.page'); - return ( -
- - - - - {({ top }) => ( -
-
-
- - {qualityGates && } -
-
-
- )} -
- {qualityGates != null && - React.Children.map(children, child => React.cloneElement(child, { organization }))} -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx new file mode 100644 index 00000000000..70d7b207032 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx @@ -0,0 +1,154 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import Helmet from 'react-helmet'; +import ListHeader from './ListHeader'; +import List from './List'; +import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; +import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; +import { fetchQualityGates } from '../../../api/quality-gates'; +import { translate } from '../../../helpers/l10n'; +import { getQualityGateUrl } from '../../../helpers/urls'; +import { Organization, QualityGate } from '../../../app/types'; +import '../styles.css'; + +interface Props { + children: React.ReactElement<{ + organization?: string; + refreshQualityGates: () => Promise; + }>; + organization: Pick; +} + +interface State { + canCreate: boolean; + loading: boolean; + qualityGates: QualityGate[]; +} + +export default class QualityGatesApp extends React.PureComponent { + mounted = false; + + static contextTypes = { + router: PropTypes.object.isRequired + }; + + state: State = { canCreate: false, loading: true, qualityGates: [] }; + + componentDidMount() { + this.mounted = true; + this.fetchQualityGates(); + + document.body.classList.add('white-page'); + document.documentElement.classList.add('white-page'); + const footer = document.getElementById('footer'); + if (footer) { + footer.classList.add('page-footer-with-sidebar'); + } + } + + componentWillUnmount() { + this.mounted = false; + document.body.classList.remove('white-page'); + document.documentElement.classList.remove('white-page'); + const footer = document.getElementById('footer'); + if (footer) { + footer.classList.remove('page-footer-with-sidebar'); + } + } + + fetchQualityGates = () => { + const { organization } = this.props; + return fetchQualityGates({ organization: organization && organization.key }).then( + ({ actions, qualitygates: qualityGates }) => { + if (this.mounted) { + this.setState({ canCreate: actions.create, loading: false, qualityGates }); + + if (qualityGates && qualityGates.length === 1 && !actions.create) { + this.context.router.replace( + getQualityGateUrl(String(qualityGates[0].id), organization && organization.key) + ); + } + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleSetDefault = (qualityGate: QualityGate) => { + this.setState(({ qualityGates }) => { + return { + qualityGates: qualityGates.map(candidate => { + if (candidate.isDefault || candidate.id === qualityGate.id) { + return { ...candidate, isDefault: candidate.id === qualityGate.id }; + } + return candidate; + }) + }; + }); + }; + + render() { + const { children } = this.props; + const { canCreate, loading, qualityGates } = this.state; + const defaultTitle = translate('quality_gates.page'); + const organization = this.props.organization && this.props.organization.key; + + return ( + <> + +
+ + + + {({ top }) => ( +
+
+
+ + {qualityGates.length > 0 && ( + + )} +
+
+
+ )} +
+ {!loading && + React.cloneElement(children, { + onSetDefault: this.handleSetDefault, + organization, + qualityGates, + refreshQualityGates: this.fetchQualityGates + })} +
+ + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx index aebd61dda73..5845c2c7cab 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx @@ -18,104 +18,80 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { QualityGate, renameQualityGate } from '../../../api/quality-gates'; -import Modal from '../../../components/controls/Modal'; -import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; +import { renameQualityGate } from '../../../api/quality-gates'; +import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; +import { QualityGate } from '../../../app/types'; interface Props { - qualityGate: QualityGate; - onRename: (qualityGate: QualityGate, newName: string) => void; - onClose: () => void; + onRename: () => Promise; organization?: string; + qualityGate: QualityGate; } interface State { - loading: boolean; name: string; } export default class RenameQualityGateForm extends React.PureComponent { - mounted = false; - constructor(props: Props) { super(props); - this.state = { loading: false, name: props.qualityGate.name }; - } - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; + this.state = { name: props.qualityGate.name }; } - handleNameChange = (event: React.SyntheticEvent) => { + handleNameChange = (event: React.ChangeEvent) => { this.setState({ name: event.currentTarget.value }); }; - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); + onRename = () => { const { qualityGate, organization } = this.props; const { name } = this.state; - if (name) { - this.setState({ loading: true }); - renameQualityGate({ id: qualityGate.id, name, organization }).then( - () => { - this.props.onRename(qualityGate, name); - this.props.onClose(); - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); + + if (!name) { + return undefined; } + + return renameQualityGate({ id: qualityGate.id, name, organization }).then(() => + this.props.onRename() + ); }; render() { const { qualityGate } = this.props; - const { loading, name } = this.state; - const header = translate('quality_gates.rename'); - const submitDisabled = loading || !name || (qualityGate && qualityGate.name === name); + const { name } = this.state; + const confirmDisable = !name || (qualityGate && qualityGate.name === name); return ( - -
-
-

{header}

+ + +
-
-
- - -
-
-
- {loading && } - - {translate('rename')} - - - {translate('cancel')} - -
- -
+ } + modalHeader={translate('quality_gates.rename')} + onConfirm={this.onRename}> + {({ onClick }) => ( + + )} + ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.tsx index 2dda968c3e0..f5c328f348a 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ThresholdInput.tsx @@ -74,12 +74,12 @@ export default class ThresholdInput extends React.PureComponent { return ( ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx index e52447250fe..8b8f0780d55 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ThresholdInput-test.tsx @@ -26,7 +26,7 @@ describe('on strings', () => { const metric = { id: '1', key: 'foo', name: 'Foo', type: 'INTEGER' }; it('should render text input', () => { const input = shallow( - + ).find('input'); expect(input.length).toEqual(1); expect(input.prop('name')).toEqual('foo'); @@ -36,7 +36,7 @@ describe('on strings', () => { it('should change', () => { const onChange = jest.fn(); const input = shallow( - + ).find('input'); change(input, 'bar'); expect(onChange).toBeCalledWith('bar'); @@ -47,7 +47,7 @@ describe('on ratings', () => { const metric = { id: '1', key: 'foo', name: 'Foo', type: 'RATING' }; it('should render Select', () => { const select = shallow( - + ).find('Select'); expect(select.length).toEqual(1); expect(select.prop('value')).toEqual('2'); @@ -56,7 +56,7 @@ describe('on ratings', () => { it('should set', () => { const onChange = jest.fn(); const select = shallow( - + ).find('Select'); (select.prop('onChange') as Function)({ label: 'D', value: '4' }); expect(onChange).toBeCalledWith('4'); @@ -65,7 +65,7 @@ describe('on ratings', () => { it('should unset', () => { const onChange = jest.fn(); const select = shallow( - + ).find('Select'); (select.prop('onChange') as Function)(null); expect(onChange).toBeCalledWith(''); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js b/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js deleted file mode 100644 index 224264d378f..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { connect } from 'react-redux'; -import { - deleteQualityGate, - showQualityGate, - renameQualityGate, - copyQualityGate, - setQualityGateAsDefault, - addCondition, - deleteCondition, - saveCondition -} from '../store/actions'; -import Details from '../components/Details'; -import { getMetrics, getQualityGatesAppState } from '../../../store/rootReducer'; -import { fetchMetrics } from '../../../store/rootActions'; - -const mapStateToProps = state => ({ - ...getQualityGatesAppState(state), - metrics: getMetrics(state) -}); - -const mapDispatchToProps = dispatch => ({ - onShow: qualityGate => dispatch(showQualityGate(qualityGate)), - onDelete: qualityGate => dispatch(deleteQualityGate(qualityGate)), - onRename: (qualityGate, newName) => dispatch(renameQualityGate(qualityGate, newName)), - onCopy: qualityGate => dispatch(copyQualityGate(qualityGate)), - onSetAsDefault: qualityGate => dispatch(setQualityGateAsDefault(qualityGate)), - onAddCondition: metric => dispatch(addCondition(metric)), - onSaveCondition: (oldCondition, newCondition) => - dispatch(saveCondition(oldCondition, newCondition)), - onDeleteCondition: condition => dispatch(deleteCondition(condition)), - fetchMetrics: () => dispatch(fetchMetrics()) -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Details); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/containers/QualityGatesAppContainer.js b/server/sonar-web/src/main/js/apps/quality-gates/containers/QualityGatesAppContainer.js deleted file mode 100644 index 052674279ea..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/containers/QualityGatesAppContainer.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { connect } from 'react-redux'; -import { setState, addQualityGate, deleteQualityGate } from '../store/actions'; -import QualityGateApp from '../components/QualityGatesApp'; -import { getQualityGatesAppState } from '../../../store/rootReducer'; - -const mapStateToProps = state => getQualityGatesAppState(state); - -const mapDispatchToProps = dispatch => ({ - updateStore: nextState => dispatch(setState(nextState)), - addQualityGate: qualityGate => dispatch(addQualityGate(qualityGate)), - deleteQualityGate: qualityGate => dispatch(deleteQualityGate(qualityGate)) -}); - -export default connect(mapStateToProps, mapDispatchToProps)(QualityGateApp); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/routes.ts b/server/sonar-web/src/main/js/apps/quality-gates/routes.ts index c173e2762b5..4d9769f5a70 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/routes.ts +++ b/server/sonar-web/src/main/js/apps/quality-gates/routes.ts @@ -22,7 +22,7 @@ import { RouterState, IndexRouteProps, RouteComponent } from 'react-router'; const routes = [ { getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { - import('./containers/QualityGatesAppContainer').then(i => callback(null, i.default)); + import('./components/QualityGatesApp').then(i => callback(null, i.default)); }, childRoutes: [ { @@ -33,7 +33,7 @@ const routes = [ { path: 'show/:id', getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { - import('./containers/DetailsContainer').then(i => callback(null, i.default)); + import('./components/DetailsApp').then(i => callback(null, i.default)); } } ] diff --git a/server/sonar-web/src/main/js/apps/quality-gates/store/actions.js b/server/sonar-web/src/main/js/apps/quality-gates/store/actions.js deleted file mode 100644 index 17b95800811..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/store/actions.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -export const SET_STATE = 'qualityGates/SET_STATE'; -export function setState(nextState) { - return { - type: SET_STATE, - nextState - }; -} - -export const ADD = 'qualityGates/ADD'; -export function addQualityGate(qualityGate) { - return { - type: ADD, - qualityGate - }; -} - -export const DELETE = 'qualityGates/DELETE'; -export function deleteQualityGate(qualityGate) { - return { - type: DELETE, - qualityGate - }; -} - -export const SHOW = 'qualityGates/SHOW'; -export function showQualityGate(qualityGate) { - return { - type: SHOW, - qualityGate - }; -} - -export const RENAME = 'qualityGates/RENAME'; -export function renameQualityGate(qualityGate, newName) { - return { - type: RENAME, - qualityGate, - newName - }; -} - -export const COPY = 'qualityGates/COPY'; -export function copyQualityGate(qualityGate) { - return { - type: COPY, - qualityGate - }; -} - -export const SET_AS_DEFAULT = 'qualityGates/SET_AS_DEFAULT'; -export function setQualityGateAsDefault(qualityGate) { - return { - type: SET_AS_DEFAULT, - qualityGate - }; -} - -export const ADD_CONDITION = 'qualityGates/ADD_CONDITION'; -export function addCondition(metric) { - return { - type: ADD_CONDITION, - metric - }; -} - -export const SAVE_CONDITION = 'qualityGates/SAVE_CONDITION'; -export function saveCondition(oldCondition, newCondition) { - return { - type: SAVE_CONDITION, - oldCondition, - newCondition - }; -} - -export const DELETE_CONDITION = 'qualityGates/DELETE_CONDITION'; -export function deleteCondition(condition) { - return { - type: DELETE_CONDITION, - condition - }; -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/store/rootReducer.js b/server/sonar-web/src/main/js/apps/quality-gates/store/rootReducer.js deleted file mode 100644 index c9c4814bf8f..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/store/rootReducer.js +++ /dev/null @@ -1,98 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { - SET_STATE, - ADD, - DELETE, - SHOW, - RENAME, - COPY, - SET_AS_DEFAULT, - ADD_CONDITION, - DELETE_CONDITION, - SAVE_CONDITION -} from './actions'; -import { checkIfDefault, addCondition, deleteCondition, replaceCondition } from './utils'; - -const initialState = {}; - -export default function rootReducer(state = initialState, action = {}) { - switch (action.type) { - case SET_STATE: - return { ...state, ...action.nextState }; - case ADD: - case COPY: - return { ...state, qualityGates: [...state.qualityGates, action.qualityGate] }; - case DELETE: - return { - ...state, - qualityGates: state.qualityGates.filter( - candidate => candidate.id !== action.qualityGate.id - ), - qualityGate: state.qualityGate.id === action.qualityGate.id ? undefined : state.qualityGate - }; - case SHOW: - return { - ...state, - qualityGate: { - ...action.qualityGate, - isDefault: checkIfDefault(action.qualityGate, state.qualityGates) - } - }; - case RENAME: - return { - ...state, - qualityGates: state.qualityGates.map(candidate => { - return candidate.id === action.qualityGate.id - ? { ...candidate, name: action.newName } - : candidate; - }), - qualityGate: { ...state.qualityGate, name: action.newName } - }; - case SET_AS_DEFAULT: - return { - ...state, - qualityGates: state.qualityGates.map(candidate => { - return { ...candidate, isDefault: candidate.id === action.qualityGate.id }; - }), - qualityGate: { - ...action.qualityGate, - isDefault: state.qualityGate.id === action.qualityGate.id - } - }; - case ADD_CONDITION: - return { - ...state, - qualityGate: addCondition(state.qualityGate, action.metric) - }; - case DELETE_CONDITION: - return { - ...state, - qualityGate: deleteCondition(state.qualityGate, action.condition) - }; - case SAVE_CONDITION: - return { - ...state, - qualityGate: replaceCondition(state.qualityGate, action.oldCondition, action.newCondition) - }; - default: - return state; - } -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/store/utils.js b/server/sonar-web/src/main/js/apps/quality-gates/store/utils.js deleted file mode 100644 index 255c43695a4..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-gates/store/utils.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -export function checkIfDefault(qualityGate, list) { - const finding = list.find(candidate => candidate.id === qualityGate.id); - - return finding ? finding.isDefault : false; -} - -export function addCondition(qualityGate, metric) { - const condition = { - metric, - op: 'LT', - warning: '', - error: '' - }; - const oldConditions = qualityGate.conditions || []; - const conditions = [...oldConditions, condition]; - - return { ...qualityGate, conditions }; -} - -export function deleteCondition(qualityGate, condition) { - const conditions = qualityGate.conditions.filter(candidate => candidate !== condition); - - return { ...qualityGate, conditions }; -} - -export function replaceCondition(qualityGate, oldCondition, newCondition) { - const conditions = qualityGate.conditions.map(candidate => { - return candidate === oldCondition ? newCondition : candidate; - }); - return { ...qualityGate, conditions }; -} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts new file mode 100644 index 00000000000..845d6da5594 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { QualityGate, Condition } from '../../app/types'; + +export function checkIfDefault(qualityGate: QualityGate, list: QualityGate[]): boolean { + const finding = list.find(candidate => candidate.id === qualityGate.id); + return (finding && finding.isDefault) || false; +} + +export function addCondition(qualityGate: QualityGate, metric: string): QualityGate { + const condition: Condition = { + metric, + op: 'LT', + warning: '', + error: '' + }; + const oldConditions = qualityGate.conditions || []; + const conditions = [...oldConditions, condition]; + return { ...qualityGate, conditions }; +} + +export function deleteCondition(qualityGate: QualityGate, condition: Condition): QualityGate { + const conditions = + qualityGate.conditions && qualityGate.conditions.filter(candidate => candidate !== condition); + return { ...qualityGate, conditions }; +} + +export function replaceCondition( + qualityGate: QualityGate, + newCondition: Condition, + oldCondition: Condition +): QualityGate { + const conditions = + qualityGate.conditions && + qualityGate.conditions.map(candidate => { + return candidate === oldCondition ? newCondition : candidate; + }); + return { ...qualityGate, conditions }; +} diff --git a/server/sonar-web/src/main/js/components/SelectList/SelectListListElement.tsx b/server/sonar-web/src/main/js/components/SelectList/SelectListListElement.tsx index 1d893f211b3..8fa07588bb2 100644 --- a/server/sonar-web/src/main/js/components/SelectList/SelectListListElement.tsx +++ b/server/sonar-web/src/main/js/components/SelectList/SelectListListElement.tsx @@ -61,7 +61,7 @@ export default class SelectListListElement extends React.PureComponent +
  • +
  • +
  • a { + pointer-events: none; +} + .select-list-list-item { display: inline-block; vertical-align: middle; diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx index 354e11add9c..6d6f558bdcb 100644 --- a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx +++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx @@ -32,6 +32,7 @@ interface Props { children: (props: ChildrenProps) => React.ReactNode; confirmButtonText: string; confirmData?: string; + confirmDisable?: boolean; isDestructive?: boolean; modalBody: React.ReactNode; modalHeader: string; @@ -82,7 +83,7 @@ export default class ConfirmButton extends React.PureComponent { }; render() { - const { confirmButtonText, isDestructive, modalBody, modalHeader } = this.props; + const { confirmButtonText, confirmDisable, isDestructive, modalBody, modalHeader } = this.props; return ( <> @@ -107,7 +108,7 @@ export default class ConfirmButton extends React.PureComponent { + disabled={submitting || confirmDisable}> {confirmButtonText} diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 51ea1ad9971..15c9d1ac30b 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -30,7 +30,6 @@ import organizationsMembers, * as fromOrganizationsMembers from './organizations import globalMessages, * as fromGlobalMessages from './globalMessages/duck'; import permissionsApp, * as fromPermissionsApp from '../apps/permissions/shared/store/rootReducer'; import projectAdminApp, * as fromProjectAdminApp from '../apps/project-admin/store/rootReducer'; -import qualityGatesApp from '../apps/quality-gates/store/rootReducer'; import settingsApp, * as fromSettingsApp from '../apps/settings/store/rootReducer'; export default combineReducers({ @@ -48,7 +47,6 @@ export default combineReducers({ // apps permissionsApp, projectAdminApp, - qualityGatesApp, settingsApp }); @@ -124,8 +122,6 @@ export const getOrganizationMembersLogins = (state, organization) => export const getOrganizationMembersState = (state, organization) => fromOrganizationsMembers.getOrganizationMembersState(state.organizationsMembers, organization); -export const getQualityGatesAppState = state => state.qualityGatesApp; - export const getPermissionsAppUsers = state => fromPermissionsApp.getUsers(state.permissionsApp); export const getPermissionsAppGroups = state => fromPermissionsApp.getGroups(state.permissionsApp); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 824627768bf..ce41b78b1fe 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1154,7 +1154,6 @@ quality_gates.no_conditions=No Conditions quality_gates.introduction=Only project measures are checked against thresholds. Sub-projects, directories and files are ignored. quality_gates.health_icons=Project health icons represent: quality_gates.projects_for_default=Every project not specifically associated to a quality gate will be associated to this one by default. -quality_gates.projects_for_default.edit=You must not select specific projects for the default quality gate. quality_gates.projects.with=With quality_gates.projects.without=Without quality_gates.projects.all=All