From c9b7be242b0cc8513923d6a38c4c957aa25dd170 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Tue, 8 May 2018 08:43:32 +0200 Subject: [PATCH] SONAR-10513 Remove redux store form quality gates app --- .../src/main/js/api/quality-gates.ts | 41 +--- .../main/js/app/styles/components/menu.css | 4 + .../src/main/js/app/styles/init/type.css | 2 +- server/sonar-web/src/main/js/app/types.ts | 25 +++ .../main/js/apps/projectQualityGate/App.tsx | 7 +- .../main/js/apps/projectQualityGate/Form.tsx | 2 +- .../components/AddConditionForm.js | 68 ------- .../components/AddConditionSelect.tsx | 80 ++++++++ .../quality-gates/components/Condition.tsx | 137 +++++-------- .../{Conditions.js => Conditions.tsx} | 105 +++++----- .../components/CopyQualityGateForm.tsx | 120 +++++------- .../components/CreateQualityGateForm.tsx | 116 +++++------ .../components/DeleteConditionForm.tsx | 86 +++------ .../components/DeleteQualityGateForm.tsx | 88 +++------ .../apps/quality-gates/components/Details.js | 80 -------- .../quality-gates/components/DetailsApp.tsx | 180 ++++++++++++++++++ .../{DetailsContent.js => DetailsContent.tsx} | 46 +++-- .../components/DetailsHeader.tsx | 95 +++------ .../components/{Intro.js => Intro.tsx} | 2 +- .../components/{List.js => List.tsx} | 12 +- .../quality-gates/components/ListHeader.tsx | 61 ++---- .../components/{Projects.js => Projects.tsx} | 37 ++-- .../components/QualityGatesApp.js | 107 ----------- .../components/QualityGatesApp.tsx | 154 +++++++++++++++ .../components/RenameQualityGateForm.tsx | 116 +++++------ .../components/ThresholdInput.tsx | 6 +- .../__tests__/ThresholdInput-test.tsx | 10 +- .../containers/DetailsContainer.js | 53 ------ .../containers/QualityGatesAppContainer.js | 33 ---- .../src/main/js/apps/quality-gates/routes.ts | 4 +- .../js/apps/quality-gates/store/actions.js | 100 ---------- .../apps/quality-gates/store/rootReducer.js | 98 ---------- .../{store/utils.js => utils.ts} | 33 ++-- .../SelectList/SelectListListElement.tsx | 2 +- .../SelectListListElement-test.tsx.snap | 8 +- .../main/js/components/SelectList/styles.css | 8 + .../js/components/controls/ConfirmButton.tsx | 5 +- .../src/main/js/store/rootReducer.js | 4 - .../resources/org/sonar/l10n/core.properties | 1 - 39 files changed, 914 insertions(+), 1222 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionForm.js create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/AddConditionSelect.tsx rename server/sonar-web/src/main/js/apps/quality-gates/components/{Conditions.js => Conditions.tsx} (65%) delete mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/Details.js create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx rename server/sonar-web/src/main/js/apps/quality-gates/components/{DetailsContent.js => DetailsContent.tsx} (57%) rename server/sonar-web/src/main/js/apps/quality-gates/components/{Intro.js => Intro.tsx} (97%) rename server/sonar-web/src/main/js/apps/quality-gates/components/{List.js => List.tsx} (88%) rename server/sonar-web/src/main/js/apps/quality-gates/components/{Projects.js => Projects.tsx} (79%) delete mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx delete mode 100644 server/sonar-web/src/main/js/apps/quality-gates/containers/DetailsContainer.js delete mode 100644 server/sonar-web/src/main/js/apps/quality-gates/containers/QualityGatesAppContainer.js delete mode 100644 server/sonar-web/src/main/js/apps/quality-gates/store/actions.js delete mode 100644 server/sonar-web/src/main/js/apps/quality-gates/store/rootReducer.js rename server/sonar-web/src/main/js/apps/quality-gates/{store/utils.js => utils.ts} (59%) 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.tsx similarity index 65% rename from server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js rename to server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index 982d6d9f2ab..0df36b5110f 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -17,54 +17,58 @@ * 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 * 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'; -function getKey(condition, index) { - return condition.id ? condition.id : `new-${index}`; +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; } -export default class Conditions extends React.PureComponent { - state = { - error: null - }; +interface State { + error?: string; +} + +export default class Conditions extends React.PureComponent { + state: State = {}; - componentWillUpdate(nextProps) { + componentWillUpdate(nextProps: Props) { if (nextProps.qualityGate !== this.props.qualityGate) { - this.setState({ error: null }); + this.setState({ error: undefined }); } } - handleError(error) { - try { - error.response.json().then(r => { - const message = r.errors.map(e => e.msg).join('. '); + getConditionKey = (condition: ICondition, index: number) => { + return condition.id ? condition.id : `new-${index}`; + }; + + handleError = (error: any) => { + parseError(error).then( + message => { this.setState({ error: message }); - }); - } catch (ex) { - this.setState({ error: translate('default_error_message') }); - } - } + }, + () => {} + ); + }; - handleResetError() { - this.setState({ error: null }); - } + handleResetError = () => { + this.setState({ error: undefined }); + }; render() { - const { - qualityGate, - conditions, - metrics, - edit, - onAddCondition, - onSaveCondition, - onDeleteCondition, - organization - } = this.props; + const { qualityGate, conditions, metrics, canEdit, organization } = this.props; const existingConditions = conditions.filter(condition => metrics[condition.metric]); @@ -73,7 +77,7 @@ export default class Conditions extends React.PureComponent { condition => metrics[condition.metric] && metrics[condition.metric].name ); - const duplicates = []; + const duplicates: ICondition[] = []; const savedConditions = existingConditions.filter(condition => condition.id != null); savedConditions.forEach(condition => { const sameCount = savedConditions.filter( @@ -88,6 +92,15 @@ export default class Conditions extends React.PureComponent { ...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 (
@@ -119,22 +132,23 @@ export default class Conditions extends React.PureComponent { {translate('quality_gates.conditions.operator')} {translate('quality_gates.conditions.warning')} {translate('quality_gates.conditions.error')} - {edit && } + {canEdit && } {sortedConditions.map((condition, index) => ( ))} @@ -143,7 +157,12 @@ export default class Conditions extends React.PureComponent {
{translate('quality_gates.no_conditions')}
)} - {edit && } + {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.tsx similarity index 57% rename from server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js rename to server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx index 35b37f12bae..7463d772635 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx @@ -17,48 +17,54 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +import * as React from 'react'; import 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'; -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 || {}; +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; +} - const defaultMessage = actions.associateProjects - ? translate('quality_gates.projects_for_default.edit') - : translate('quality_gates.projects_for_default'); +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')}

- {gate.isDefault ? ( - defaultMessage + {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.tsx similarity index 97% rename from server/sonar-web/src/main/js/apps/quality-gates/components/Intro.js rename to server/sonar-web/src/main/js/apps/quality-gates/components/Intro.tsx index 37fb224b0f8..ea5edc9180c 100644 --- 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.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +import * as React from 'react'; import { translate } from '../../../helpers/l10n'; export default function Intro() { 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.tsx similarity index 88% rename from server/sonar-web/src/main/js/apps/quality-gates/components/List.js rename to server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx index a1319df5af5..5586c52f22f 100644 --- 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.tsx @@ -17,13 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +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'; -export default function List({ organization, qualityGates }) { +interface Props { + organization?: string; + qualityGates: QualityGate[]; +} + +export default function List({ organization, qualityGates }: Props) { return (
{qualityGates.map(qualityGate => ( @@ -32,7 +38,7 @@ export default function List({ organization, qualityGates }) { className="list-group-item" data-id={qualityGate.id} key={qualityGate.id} - to={getQualityGateUrl(String(qualityGate.id), organization && organization.key)}> + to={getQualityGateUrl(String(qualityGate.id), organization)}> 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.tsx similarity index 79% rename from server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js rename to server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx index b51bed9bd11..8153ec3e3ca 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +import * as React from 'react'; import { find, without } from 'lodash'; import SelectList, { Filter } from '../../../components/SelectList/SelectList'; import { translate } from '../../../helpers/l10n'; @@ -26,23 +26,27 @@ import { associateGateWithProject, dissociateGateWithProject } from '../../../api/quality-gates'; -/*:: import { Project } from '../../projects/types'; */ +import { QualityGate } from '../../../app/types'; -/*:: -type State = { - projects: Projects[], - selectedProjects: string[] -}; -*/ +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: [] }; +export default class Projects extends React.PureComponent { + state: State = { projects: [], selectedProjects: [] }; componentDidMount() { this.handleSearch('', Filter.Selected); } - handleSearch = (query /*: string*/, selected /*: string */) => { + handleSearch = (query: string, selected: string) => { return searchGates({ gateId: this.props.qualityGate.id, organization: this.props.organization, @@ -59,26 +63,26 @@ export default class Projects extends React.PureComponent { }); }; - handleSelect = (id /*: string*/) => { + handleSelect = (id: string) => { return associateGateWithProject({ gateId: this.props.qualityGate.id, organization: this.props.organization, projectId: id }).then(() => { - this.setState((state /*: State*/) => ({ + this.setState(state => ({ selectedProjects: [...state.selectedProjects, id] })); }); }; - handleUnselect = (id /*: string*/) => { + handleUnselect = (id: string) => { return dissociateGateWithProject({ gateId: this.props.qualityGate.id, organization: this.props.organization, projectId: id }).then( () => { - this.setState((state /*: State*/) => ({ + this.setState(state => ({ selectedProjects: without(state.selectedProjects, id) })); }, @@ -86,7 +90,7 @@ export default class Projects extends React.PureComponent { ); }; - renderElement = (id /*: string*/) /*: React.ReactNode*/ => { + renderElement = (id: string): React.ReactNode => { const project = find(this.state.projects, { id }); return project === undefined ? id : project.name; }; @@ -101,6 +105,7 @@ export default class Projects extends React.PureComponent { 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/utils.ts similarity index 59% rename from server/sonar-web/src/main/js/apps/quality-gates/store/utils.js rename to server/sonar-web/src/main/js/apps/quality-gates/utils.ts index 255c43695a4..845d6da5594 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/store/utils.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts @@ -17,14 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export function checkIfDefault(qualityGate, list) { - const finding = list.find(candidate => candidate.id === qualityGate.id); - return finding ? finding.isDefault : false; +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, metric) { - const condition = { +export function addCondition(qualityGate: QualityGate, metric: string): QualityGate { + const condition: Condition = { metric, op: 'LT', warning: '', @@ -32,19 +34,24 @@ export function addCondition(qualityGate, metric) { }; const oldConditions = qualityGate.conditions || []; const conditions = [...oldConditions, condition]; - return { ...qualityGate, conditions }; } -export function deleteCondition(qualityGate, condition) { - const conditions = qualityGate.conditions.filter(candidate => candidate !== condition); - +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, oldCondition, newCondition) { - const conditions = qualityGate.conditions.map(candidate => { - return candidate === oldCondition ? newCondition : candidate; - }); +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 -- 2.39.5