@@ -22,7 +22,7 @@ import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { BranchParameters } from '../types/branch-like'; | |||
import { QualityGateApplicationStatus, QualityGateProjectStatus } from '../types/quality-gates'; | |||
export function fetchQualityGates(data: { | |||
export function fetchQualityGates(data?: { | |||
organization?: string; | |||
}): Promise<{ | |||
actions: { create: boolean }; |
@@ -434,7 +434,7 @@ th.huge-spacer-right { | |||
.display-flex-start { | |||
display: flex !important; | |||
align-items: flex-start; | |||
align-items: flex-start !important; | |||
} | |||
.display-flex-end { |
@@ -1,147 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { Helmet } from 'react-helmet-async'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { | |||
associateGateWithProject, | |||
dissociateGateWithProject, | |||
fetchQualityGates, | |||
getGateForProject | |||
} from '../../api/quality-gates'; | |||
import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; | |||
import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | |||
import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; | |||
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; | |||
import Form from './Form'; | |||
import Header from './Header'; | |||
interface Props { | |||
component: T.Component; | |||
onComponentChange: (changes: {}) => void; | |||
} | |||
interface State { | |||
allGates?: T.QualityGate[]; | |||
gate?: T.QualityGate; | |||
loading: boolean; | |||
} | |||
export default class App extends React.PureComponent<Props> { | |||
mounted = false; | |||
state: State = { loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
if (this.checkPermissions()) { | |||
this.fetchQualityGates(); | |||
} else { | |||
handleRequiredAuthorization(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
checkPermissions() { | |||
const { configuration } = this.props.component; | |||
const hasPermission = configuration && configuration.showQualityGates; | |||
return !!hasPermission; | |||
} | |||
fetchQualityGates() { | |||
const { component } = this.props; | |||
this.setState({ loading: true }); | |||
Promise.all([ | |||
fetchQualityGates({ organization: component.organization }), | |||
getGateForProject({ organization: component.organization, project: component.key }) | |||
]).then( | |||
([qualityGateList, gate]) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
allGates: qualityGateList?.qualitygates, | |||
gate, | |||
loading: false | |||
}); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
} | |||
handleChangeGate = (oldId?: string, newId?: string) => { | |||
const { allGates } = this.state; | |||
if ((!oldId && !newId) || !allGates) { | |||
return Promise.resolve(); | |||
} | |||
const { component } = this.props; | |||
const requestData = { | |||
gateId: newId ? newId : oldId!, | |||
organization: component.organization, | |||
projectKey: component.key | |||
}; | |||
const request = newId | |||
? associateGateWithProject(requestData) | |||
: dissociateGateWithProject(requestData); | |||
return request.then(() => { | |||
if (this.mounted) { | |||
addGlobalSuccessMessage(translate('project_quality_gate.successfully_updated')); | |||
if (newId) { | |||
const newGate = allGates.find(gate => gate.id === newId); | |||
if (newGate) { | |||
this.setState({ gate: newGate }); | |||
this.props.onComponentChange({ qualityGate: newGate }); | |||
} | |||
} else { | |||
this.setState({ gate: undefined }); | |||
} | |||
} | |||
}); | |||
}; | |||
render() { | |||
if (!this.checkPermissions()) { | |||
return null; | |||
} | |||
const { allGates, gate, loading } = this.state; | |||
return ( | |||
<div className="page page-limited" id="project-quality-gate"> | |||
<Suggestions suggestions="project_quality_gate" /> | |||
<Helmet defer={false} title={translate('project_quality_gate.page')} /> | |||
<A11ySkipTarget anchor="qg_main" /> | |||
<Header /> | |||
{loading ? ( | |||
<i className="spinner" /> | |||
) : ( | |||
allGates && <Form allGates={allGates} gate={gate} onChange={this.handleChangeGate} /> | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,117 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
interface Props { | |||
allGates: T.QualityGate[]; | |||
gate?: T.QualityGate; | |||
onChange: (oldGate?: string, newGate?: string) => Promise<void>; | |||
} | |||
interface State { | |||
loading: boolean; | |||
} | |||
interface Option { | |||
isDefault?: boolean; | |||
label: string; | |||
value: string; | |||
} | |||
export default class Form extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
stopLoading = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
handleChange = (option: { value: string }) => { | |||
const { gate } = this.props; | |||
const isSet = gate == null && option.value != null; | |||
const isUnset = gate != null && option.value == null; | |||
const isChanged = gate != null && gate.id !== option.value; | |||
const hasChanged = isSet || isUnset || isChanged; | |||
if (hasChanged) { | |||
this.setState({ loading: true }); | |||
this.props.onChange(gate && gate.id, option.value).then(this.stopLoading, this.stopLoading); | |||
} | |||
}; | |||
renderGateName = (option: { isDefault?: boolean; label: string }) => { | |||
if (option.isDefault) { | |||
return ( | |||
<span> | |||
<strong>{translate('default')}</strong> | |||
{': '} | |||
{option.label} | |||
</span> | |||
); | |||
} | |||
return <span>{option.label}</span>; | |||
}; | |||
renderSelect() { | |||
const { gate, allGates } = this.props; | |||
const options: Option[] = allGates.map(gate => ({ | |||
value: String(gate.id), | |||
label: gate.name, | |||
isDefault: gate.isDefault | |||
})); | |||
return ( | |||
<Select | |||
clearable={false} | |||
disabled={this.state.loading} | |||
onChange={this.handleChange} | |||
optionRenderer={this.renderGateName} | |||
options={options} | |||
style={{ width: 300 }} | |||
value={gate && String(gate.id)} | |||
valueRenderer={this.renderGateName} | |||
/> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<div> | |||
{this.renderSelect()} | |||
{this.state.loading && <i className="spinner spacer-left" />} | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,41 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
export default function Header() { | |||
return ( | |||
<header className="page-header"> | |||
<div className="page-title display-flex-center"> | |||
<h1>{translate('project_quality_gate.page')}</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div className="big-padded-top big-padded-bottom"> | |||
{translate('quality_gates.projects.help')} | |||
</div> | |||
} | |||
/> | |||
</div> | |||
<div className="page-description">{translate('project_quality_gate.page.description')}</div> | |||
</header> | |||
); | |||
} |
@@ -0,0 +1,192 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 'sonar-ui-common/helpers/l10n'; | |||
import { | |||
associateGateWithProject, | |||
dissociateGateWithProject, | |||
fetchQualityGates, | |||
getGateForProject, | |||
searchProjects | |||
} from '../../api/quality-gates'; | |||
import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; | |||
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; | |||
import { USE_SYSTEM_DEFAULT } from './constants'; | |||
import ProjectQualityGateAppRenderer from './ProjectQualityGateAppRenderer'; | |||
interface Props { | |||
component: T.Component; | |||
onComponentChange: (changes: {}) => void; | |||
} | |||
interface State { | |||
allQualityGates?: T.QualityGate[]; | |||
currentQualityGate?: T.QualityGate; | |||
loading: boolean; | |||
selectedQualityGateId: string; | |||
submitting: boolean; | |||
} | |||
export default class ProjectQualityGateApp extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
loading: true, | |||
selectedQualityGateId: USE_SYSTEM_DEFAULT, | |||
submitting: false | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
if (this.checkPermissions()) { | |||
this.fetchQualityGates(); | |||
} else { | |||
handleRequiredAuthorization(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
checkPermissions = () => { | |||
const { configuration } = this.props.component; | |||
const hasPermission = configuration && configuration.showQualityGates; | |||
return !!hasPermission; | |||
}; | |||
isUsingDefault = async (qualityGate: T.QualityGate) => { | |||
const { component } = this.props; | |||
if (!qualityGate.isDefault) { | |||
return false; | |||
} else { | |||
// If this is the default Quality Gate, check if it was explicitly | |||
// selected, or if we're inheriting the system default. | |||
/* eslint-disable-next-line sonarjs/prefer-immediate-return */ | |||
const selected = await searchProjects({ | |||
gateName: qualityGate.name, | |||
query: component.key | |||
}) | |||
.then(({ results }) => { | |||
return Boolean(results.find(r => r.key === component.key)?.selected); | |||
}) | |||
.catch(() => false); | |||
// If it's NOT selected, it means we're following the system default. | |||
return !selected; | |||
} | |||
}; | |||
fetchQualityGates = async () => { | |||
const { component } = this.props; | |||
this.setState({ loading: true }); | |||
const [allQualityGates, currentQualityGate] = await Promise.all([ | |||
fetchQualityGates().then(({ qualitygates }) => qualitygates), | |||
getGateForProject({ project: component.key }) | |||
]).catch(() => []); | |||
if (allQualityGates && currentQualityGate) { | |||
const usingDefault = await this.isUsingDefault(currentQualityGate); | |||
if (this.mounted) { | |||
this.setState({ | |||
allQualityGates, | |||
currentQualityGate, | |||
selectedQualityGateId: usingDefault ? USE_SYSTEM_DEFAULT : currentQualityGate.id, | |||
loading: false | |||
}); | |||
} | |||
} else if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
handleSelect = (selectedQualityGateId: string) => { | |||
this.setState({ selectedQualityGateId }); | |||
}; | |||
handleSubmit = async () => { | |||
const { component } = this.props; | |||
const { allQualityGates, currentQualityGate, selectedQualityGateId } = this.state; | |||
if (allQualityGates === undefined || currentQualityGate === undefined) { | |||
return; | |||
} | |||
this.setState({ submitting: true }); | |||
if (selectedQualityGateId === USE_SYSTEM_DEFAULT) { | |||
await dissociateGateWithProject({ | |||
gateId: currentQualityGate.id, | |||
projectKey: component.key | |||
}).catch(() => { | |||
/* noop */ | |||
}); | |||
} else { | |||
await associateGateWithProject({ | |||
gateId: selectedQualityGateId, | |||
projectKey: component.key | |||
}).catch(() => { | |||
/* noop */ | |||
}); | |||
} | |||
if (this.mounted) { | |||
addGlobalSuccessMessage(translate('project_quality_gate.successfully_updated')); | |||
const newGate = | |||
selectedQualityGateId === USE_SYSTEM_DEFAULT | |||
? allQualityGates.find(gate => gate.isDefault) | |||
: allQualityGates.find(gate => gate.id === selectedQualityGateId); | |||
if (newGate) { | |||
this.setState({ currentQualityGate: newGate, submitting: false }); | |||
this.props.onComponentChange({ qualityGate: newGate }); | |||
} | |||
} | |||
}; | |||
render() { | |||
if (!this.checkPermissions()) { | |||
return null; | |||
} | |||
const { | |||
allQualityGates, | |||
currentQualityGate, | |||
loading, | |||
selectedQualityGateId, | |||
submitting | |||
} = this.state; | |||
return ( | |||
<ProjectQualityGateAppRenderer | |||
allQualityGates={allQualityGates} | |||
currentQualityGate={currentQualityGate} | |||
loading={loading} | |||
onSubmit={this.handleSubmit} | |||
onSelect={this.handleSelect} | |||
selectedQualityGateId={selectedQualityGateId} | |||
submitting={submitting} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,166 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { Helmet } from 'react-helmet-async'; | |||
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; | |||
import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | |||
import BuiltInQualityGateBadge from '../quality-gates/components/BuiltInQualityGateBadge'; | |||
import { USE_SYSTEM_DEFAULT } from './constants'; | |||
export interface ProjectQualityGateAppRendererProps { | |||
allQualityGates?: T.QualityGate[]; | |||
currentQualityGate?: T.QualityGate; | |||
loading: boolean; | |||
onSelect: (id: string) => void; | |||
onSubmit: () => void; | |||
selectedQualityGateId: string; | |||
submitting: boolean; | |||
} | |||
export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateAppRendererProps) { | |||
const { allQualityGates, currentQualityGate, loading, selectedQualityGateId, submitting } = props; | |||
const defaultQualityGate = allQualityGates?.find(g => g.isDefault); | |||
if (loading) { | |||
return <i className="spinner" />; | |||
} | |||
if ( | |||
allQualityGates === undefined || | |||
defaultQualityGate === undefined || | |||
currentQualityGate === undefined | |||
) { | |||
return null; | |||
} | |||
const usesDefault = selectedQualityGateId === USE_SYSTEM_DEFAULT; | |||
const needsReanalysis = usesDefault | |||
? // currentQualityGate.isDefault is not always up to date. We need to check | |||
// against defaultQualityGate explicitly. | |||
defaultQualityGate.id !== currentQualityGate.id | |||
: selectedQualityGateId !== currentQualityGate.id; | |||
const options = allQualityGates.map(g => ({ | |||
label: g.name, | |||
value: g.id | |||
})); | |||
return ( | |||
<div className="page page-limited" id="project-quality-gate"> | |||
<Suggestions suggestions="project_quality_gate" /> | |||
<Helmet defer={false} title={translate('project_quality_gate.page')} /> | |||
<A11ySkipTarget anchor="qg_main" /> | |||
<header className="page-header"> | |||
<div className="page-title display-flex-center"> | |||
<h1>{translate('project_quality_gate.page')}</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div className="big-padded-top big-padded-bottom"> | |||
{translate('quality_gates.projects.help')} | |||
</div> | |||
} | |||
/> | |||
</div> | |||
</header> | |||
<div className="boxed-group"> | |||
<h2 className="boxed-group-header">{translate('project_quality_gate.subtitle')}</h2> | |||
<form | |||
className="boxed-group-inner" | |||
onSubmit={e => { | |||
e.preventDefault(); | |||
props.onSubmit(); | |||
}}> | |||
<p className="big-spacer-bottom">{translate('project_quality_gate.page.description')}</p> | |||
<div className="big-spacer-bottom"> | |||
<Radio | |||
className="display-flex-start" | |||
checked={usesDefault} | |||
disabled={submitting} | |||
onCheck={() => props.onSelect(USE_SYSTEM_DEFAULT)} | |||
value={USE_SYSTEM_DEFAULT}> | |||
<div className="spacer-left"> | |||
<div className="little-spacer-bottom"> | |||
{translate('project_quality_gate.always_use_default')} | |||
</div> | |||
<div className="display-flex-center"> | |||
<span className="text-muted little-spacer-right"> | |||
{translate('current_noun')}: | |||
</span> | |||
{defaultQualityGate.name} | |||
{defaultQualityGate.isBuiltIn && ( | |||
<BuiltInQualityGateBadge className="spacer-left" /> | |||
)} | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div className="big-spacer-bottom"> | |||
<Radio | |||
className="display-flex-start" | |||
checked={!usesDefault} | |||
disabled={submitting} | |||
onCheck={value => props.onSelect(value)} | |||
value={!usesDefault ? selectedQualityGateId : currentQualityGate.id}> | |||
<div className="spacer-left"> | |||
<div className="little-spacer-bottom"> | |||
{translate('project_quality_gate.always_use_specific')} | |||
</div> | |||
<div className="display-flex-center"> | |||
<Select | |||
className="abs-width-300" | |||
clearable={false} | |||
disabled={submitting || usesDefault} | |||
onChange={({ value }: { value: string }) => props.onSelect(value)} | |||
options={options} | |||
optionRenderer={option => <span>{option.label}</span>} | |||
value={selectedQualityGateId} | |||
/> | |||
</div> | |||
</div> | |||
</Radio> | |||
{needsReanalysis && ( | |||
<Alert className="big-spacer-top" variant="warning"> | |||
{translate('project_quality_gate.requires_new_analysis')} | |||
</Alert> | |||
)} | |||
</div> | |||
<div> | |||
<SubmitButton disabled={submitting}>{translate('save')}</SubmitButton> | |||
{submitting && <i className="spinner spacer-left" />} | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -1,145 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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. | |||
*/ | |||
/* eslint-disable import/first */ | |||
jest.mock('../../../api/quality-gates', () => ({ | |||
associateGateWithProject: jest.fn(() => Promise.resolve()), | |||
dissociateGateWithProject: jest.fn(() => Promise.resolve()), | |||
fetchQualityGates: jest.fn(() => Promise.resolve({})), | |||
getGateForProject: jest.fn(() => Promise.resolve()) | |||
})); | |||
jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ | |||
default: jest.fn() | |||
})); | |||
jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ | |||
default: jest.fn() | |||
})); | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import App from '../App'; | |||
const associateGateWithProject = require('../../../api/quality-gates') | |||
.associateGateWithProject as jest.Mock<any>; | |||
const dissociateGateWithProject = require('../../../api/quality-gates') | |||
.dissociateGateWithProject as jest.Mock<any>; | |||
const fetchQualityGates = require('../../../api/quality-gates').fetchQualityGates as jest.Mock<any>; | |||
const getGateForProject = require('../../../api/quality-gates').getGateForProject as jest.Mock<any>; | |||
const addGlobalSuccessMessage = require('../../../app/utils/addGlobalSuccessMessage') | |||
.default as jest.Mock<any>; | |||
const handleRequiredAuthorization = require('../../../app/utils/handleRequiredAuthorization') | |||
.default as jest.Mock<any>; | |||
const component = { | |||
analysisDate: '', | |||
breadcrumbs: [], | |||
configuration: { showQualityGates: true }, | |||
key: 'component', | |||
name: 'component', | |||
organization: 'org', | |||
qualifier: 'TRK', | |||
version: '0.0.1' | |||
} as T.Component; | |||
beforeEach(() => { | |||
associateGateWithProject.mockClear(); | |||
dissociateGateWithProject.mockClear(); | |||
addGlobalSuccessMessage.mockClear(); | |||
}); | |||
it('checks permissions', () => { | |||
handleRequiredAuthorization.mockClear(); | |||
shallow( | |||
<App | |||
component={{ ...component, configuration: undefined } as T.Component} | |||
onComponentChange={jest.fn()} | |||
/> | |||
); | |||
expect(handleRequiredAuthorization).toBeCalled(); | |||
}); | |||
it('fetches quality gates', () => { | |||
fetchQualityGates.mockClear(); | |||
getGateForProject.mockClear(); | |||
shallow(<App component={component} onComponentChange={jest.fn()} />); | |||
expect(fetchQualityGates).toBeCalledWith({ organization: 'org' }); | |||
expect(getGateForProject).toBeCalledWith({ organization: 'org', project: 'component' }); | |||
}); | |||
it('changes quality gate from custom to default', () => { | |||
const gate = randomGate('foo'); | |||
const allGates = [gate, randomGate('bar', true), randomGate('baz')]; | |||
const wrapper = mountRender(allGates, gate); | |||
wrapper.find('Form').prop<Function>('onChange')('foo', 'bar'); | |||
expect(associateGateWithProject).toBeCalledWith({ | |||
gateId: 'bar', | |||
organization: 'org', | |||
projectKey: 'component' | |||
}); | |||
}); | |||
it('changes quality gate from custom to custom', () => { | |||
const allGates = [randomGate('foo'), randomGate('bar', true), randomGate('baz')]; | |||
const wrapper = mountRender(allGates, randomGate('foo')); | |||
wrapper.find('Form').prop<Function>('onChange')('foo', 'baz'); | |||
expect(associateGateWithProject).toBeCalledWith({ | |||
gateId: 'baz', | |||
organization: 'org', | |||
projectKey: 'component' | |||
}); | |||
}); | |||
it('changes quality gate from custom to none', () => { | |||
const allGates = [randomGate('foo'), randomGate('bar'), randomGate('baz')]; | |||
const wrapper = mountRender(allGates, randomGate('foo')); | |||
wrapper.find('Form').prop<Function>('onChange')('foo', undefined); | |||
expect(dissociateGateWithProject).toBeCalledWith({ | |||
gateId: 'foo', | |||
organization: 'org', | |||
projectKey: 'component' | |||
}); | |||
}); | |||
it('changes quality gate from none to custom', () => { | |||
const allGates = [randomGate('foo'), randomGate('bar'), randomGate('baz')]; | |||
const wrapper = mountRender(allGates); | |||
wrapper.find('Form').prop<Function>('onChange')(undefined, 'baz'); | |||
expect(associateGateWithProject).toBeCalledWith({ | |||
gateId: 'baz', | |||
organization: 'org', | |||
projectKey: 'component' | |||
}); | |||
}); | |||
function randomGate(id: string, isDefault = false) { | |||
return { id, isDefault, name: id }; | |||
} | |||
function mountRender(allGates: any[], gate?: any) { | |||
const wrapper = shallow(<App component={component} onComponentChange={jest.fn()} />); | |||
wrapper.setState({ allGates, loading: false, gate }); | |||
return wrapper; | |||
} |
@@ -1,48 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import Form from '../Form'; | |||
it('renders', () => { | |||
const foo = randomGate('1'); | |||
const allGates = [foo, randomGate('2')]; | |||
expect(shallow(<Form allGates={allGates} gate={foo} onChange={jest.fn()} />)).toMatchSnapshot(); | |||
}); | |||
it('changes quality gate', () => { | |||
const allGates = [randomGate('1'), randomGate('2')]; | |||
const onChange = jest.fn(() => Promise.resolve()); | |||
const wrapper = shallow(<Form allGates={allGates} onChange={onChange} />); | |||
wrapper.find('Select').prop<Function>('onChange')({ value: '2' }); | |||
expect(onChange).lastCalledWith(undefined, '2'); | |||
wrapper.setProps({ gate: randomGate('1') }); | |||
wrapper.find('Select').prop<Function>('onChange')({ value: '2' }); | |||
expect(onChange).lastCalledWith('1', '2'); | |||
}); | |||
function randomGate(id: string) { | |||
return { | |||
id, | |||
name: `name-${id}` | |||
}; | |||
} |
@@ -0,0 +1,178 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { | |||
associateGateWithProject, | |||
dissociateGateWithProject, | |||
fetchQualityGates, | |||
getGateForProject, | |||
searchProjects | |||
} from '../../../api/quality-gates'; | |||
import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; | |||
import { mockQualityGate } from '../../../helpers/mocks/quality-gates'; | |||
import { mockComponent } from '../../../helpers/testMocks'; | |||
import { USE_SYSTEM_DEFAULT } from '../constants'; | |||
import ProjectQualityGateApp from '../ProjectQualityGateApp'; | |||
jest.mock('../../../api/quality-gates', () => { | |||
const { mockQualityGate } = jest.requireActual('../../../helpers/mocks/quality-gates'); | |||
const gate1 = mockQualityGate(); | |||
const gate2 = mockQualityGate({ id: '2', isBuiltIn: true }); | |||
const gate3 = mockQualityGate({ id: '3', isDefault: true }); | |||
return { | |||
associateGateWithProject: jest.fn().mockResolvedValue(null), | |||
dissociateGateWithProject: jest.fn().mockResolvedValue(null), | |||
fetchQualityGates: jest.fn().mockResolvedValue({ | |||
qualitygates: [gate1, gate2, gate3] | |||
}), | |||
getGateForProject: jest.fn().mockResolvedValue(gate2), | |||
searchProjects: jest.fn().mockResolvedValue({ results: [] }) | |||
}; | |||
}); | |||
jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ | |||
default: jest.fn() | |||
})); | |||
jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ | |||
default: jest.fn() | |||
})); | |||
beforeEach(jest.clearAllMocks); | |||
it('renders correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('correctly checks user permissions', () => { | |||
shallowRender({ component: mockComponent({ configuration: { showQualityGates: false } }) }); | |||
expect(handleRequiredAuthorization).toBeCalled(); | |||
}); | |||
it('correctly loads Quality Gate data', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(fetchQualityGates).toBeCalled(); | |||
expect(getGateForProject).toBeCalledWith({ project: 'foo' }); | |||
expect(wrapper.state().allQualityGates).toHaveLength(3); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('2'); | |||
expect(wrapper.state().selectedQualityGateId).toBe('2'); | |||
}); | |||
it('correctly fallbacks to the default Quality Gate', async () => { | |||
(getGateForProject as jest.Mock).mockResolvedValueOnce( | |||
mockQualityGate({ id: '3', isDefault: true }) | |||
); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchProjects).toBeCalled(); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('3'); | |||
expect(wrapper.state().selectedQualityGateId).toBe(USE_SYSTEM_DEFAULT); | |||
}); | |||
it('correctly detects if the default Quality Gate was explicitly selected', async () => { | |||
(getGateForProject as jest.Mock).mockResolvedValueOnce( | |||
mockQualityGate({ id: '3', isDefault: true }) | |||
); | |||
(searchProjects as jest.Mock).mockResolvedValueOnce({ | |||
results: [{ key: 'foo', selected: true }] | |||
}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchProjects).toBeCalled(); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('3'); | |||
expect(wrapper.state().selectedQualityGateId).toBe('3'); | |||
}); | |||
it('correctly associates a selected Quality Gate', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleSelect('3'); | |||
wrapper.instance().handleSubmit(); | |||
expect(associateGateWithProject).toHaveBeenCalledWith({ | |||
gateId: '3', | |||
projectKey: 'foo' | |||
}); | |||
}); | |||
it('correctly associates a project with the system default Quality Gate', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ | |||
currentQualityGate: mockQualityGate({ id: '1' }), | |||
selectedQualityGateId: USE_SYSTEM_DEFAULT | |||
}); | |||
wrapper.instance().handleSubmit(); | |||
expect(dissociateGateWithProject).toHaveBeenCalledWith({ | |||
gateId: '1', | |||
projectKey: 'foo' | |||
}); | |||
}); | |||
it("correctly doesn't do anything if the system default was selected, and the project had no prior Quality Gate associated with it", async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ currentQualityGate: undefined, selectedQualityGateId: USE_SYSTEM_DEFAULT }); | |||
wrapper.instance().handleSubmit(); | |||
expect(associateGateWithProject).not.toHaveBeenCalled(); | |||
expect(dissociateGateWithProject).not.toHaveBeenCalled(); | |||
}); | |||
it('correctly handles WS errors', async () => { | |||
(fetchQualityGates as jest.Mock).mockRejectedValueOnce(null); | |||
(getGateForProject as jest.Mock).mockRejectedValueOnce(null); | |||
(searchProjects as jest.Mock).mockRejectedValueOnce(null); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().allQualityGates).toBeUndefined(); | |||
expect(wrapper.state().currentQualityGate).toBeUndefined(); | |||
expect(wrapper.state().loading).toBe(false); | |||
const result = await wrapper.instance().isUsingDefault(mockQualityGate()); | |||
expect(result).toBe(false); | |||
}); | |||
function shallowRender(props: Partial<ProjectQualityGateApp['props']> = {}) { | |||
return shallow<ProjectQualityGateApp>( | |||
<ProjectQualityGateApp | |||
component={mockComponent({ key: 'foo', configuration: { showQualityGates: true } })} | |||
onComponentChange={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,111 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { submit } from 'sonar-ui-common/helpers/testUtils'; | |||
import { mockQualityGate } from '../../../helpers/mocks/quality-gates'; | |||
import { USE_SYSTEM_DEFAULT } from '../constants'; | |||
import ProjectQualityGateAppRenderer, { | |||
ProjectQualityGateAppRendererProps | |||
} from '../ProjectQualityGateAppRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); | |||
expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); | |||
expect( | |||
shallowRender({ | |||
currentQualityGate: mockQualityGate({ id: '2', isDefault: true }), | |||
selectedQualityGateId: USE_SYSTEM_DEFAULT | |||
}) | |||
).toMatchSnapshot('always use system default'); | |||
expect( | |||
shallowRender({ | |||
selectedQualityGateId: '5' | |||
}) | |||
).toMatchSnapshot('show warning'); | |||
expect( | |||
shallowRender({ | |||
selectedQualityGateId: USE_SYSTEM_DEFAULT | |||
}) | |||
).toMatchSnapshot('show warning if not using default'); | |||
expect(shallowRender({ allQualityGates: undefined }).type()).toBeNull(); // no quality gates | |||
}); | |||
it('should render select options correctly', () => { | |||
return new Promise(resolve => { | |||
const wrapper = shallowRender(); | |||
const render = wrapper.find(Select).props().optionRenderer; | |||
if (render) { | |||
expect(render({ value: '1', label: 'Gate 1' })).toMatchSnapshot('default'); | |||
resolve(); | |||
} | |||
}); | |||
}); | |||
it('should correctly handle changes', () => { | |||
const wrapper = shallowRender(); | |||
const onSelect = jest.fn(selectedQualityGateId => { | |||
wrapper.setProps({ selectedQualityGateId }); | |||
}); | |||
wrapper.setProps({ onSelect }); | |||
wrapper | |||
.find(Radio) | |||
.at(0) | |||
.props() | |||
.onCheck(USE_SYSTEM_DEFAULT); | |||
expect(onSelect).toHaveBeenLastCalledWith(USE_SYSTEM_DEFAULT); | |||
wrapper | |||
.find(Radio) | |||
.at(1) | |||
.props() | |||
.onCheck('1'); | |||
expect(onSelect).toHaveBeenLastCalledWith('1'); | |||
wrapper.find(Select).props().onChange!({ value: '2' }); | |||
expect(onSelect).toHaveBeenLastCalledWith('2'); | |||
}); | |||
it('should correctly handle form submission', () => { | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ onSubmit }); | |||
submit(wrapper.find('form')); | |||
expect(onSubmit).toBeCalled(); | |||
}); | |||
function shallowRender(props: Partial<ProjectQualityGateAppRendererProps> = {}) { | |||
return shallow<ProjectQualityGateAppRendererProps>( | |||
<ProjectQualityGateAppRenderer | |||
allQualityGates={[mockQualityGate(), mockQualityGate({ id: '2', isDefault: true })]} | |||
currentQualityGate={mockQualityGate({ id: '1' })} | |||
loading={false} | |||
onSelect={jest.fn()} | |||
onSubmit={jest.fn()} | |||
selectedQualityGateId="1" | |||
submitting={false} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,33 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<div> | |||
<Select | |||
clearable={false} | |||
disabled={false} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"isDefault": undefined, | |||
"label": "name-1", | |||
"value": "1", | |||
}, | |||
Object { | |||
"isDefault": undefined, | |||
"label": "name-2", | |||
"value": "2", | |||
}, | |||
] | |||
} | |||
style={ | |||
Object { | |||
"width": 300, | |||
} | |||
} | |||
value="1" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
`; |
@@ -1,30 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<header | |||
className="page-header" | |||
> | |||
<div | |||
className="page-title display-flex-center" | |||
> | |||
<h1> | |||
project_quality_gate.page | |||
</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div | |||
className="big-padded-top big-padded-bottom" | |||
> | |||
quality_gates.projects.help | |||
</div> | |||
} | |||
/> | |||
</div> | |||
<div | |||
className="page-description" | |||
> | |||
project_quality_gate.page.description | |||
</div> | |||
</header> | |||
`; |
@@ -0,0 +1,11 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders correctly 1`] = ` | |||
<ProjectQualityGateAppRenderer | |||
loading={true} | |||
onSelect={[Function]} | |||
onSubmit={[Function]} | |||
selectedQualityGateId="-1" | |||
submitting={false} | |||
/> | |||
`; |
@@ -0,0 +1,743 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: always use system default 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-quality-gate" | |||
> | |||
<Suggestions | |||
suggestions="project_quality_gate" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
title="project_quality_gate.page" | |||
/> | |||
<A11ySkipTarget | |||
anchor="qg_main" | |||
/> | |||
<header | |||
className="page-header" | |||
> | |||
<div | |||
className="page-title display-flex-center" | |||
> | |||
<h1> | |||
project_quality_gate.page | |||
</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div | |||
className="big-padded-top big-padded-bottom" | |||
> | |||
quality_gates.projects.help | |||
</div> | |||
} | |||
/> | |||
</div> | |||
</header> | |||
<div | |||
className="boxed-group" | |||
> | |||
<h2 | |||
className="boxed-group-header" | |||
> | |||
project_quality_gate.subtitle | |||
</h2> | |||
<form | |||
className="boxed-group-inner" | |||
onSubmit={[Function]} | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
project_quality_gate.page.description | |||
</p> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={true} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="-1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_default | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="text-muted little-spacer-right" | |||
> | |||
current_noun | |||
: | |||
</span> | |||
qualitygate | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={false} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="2" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_specific | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Select | |||
className="abs-width-300" | |||
clearable={false} | |||
disabled={true} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
] | |||
} | |||
value="-1" | |||
/> | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
save | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-quality-gate" | |||
> | |||
<Suggestions | |||
suggestions="project_quality_gate" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
title="project_quality_gate.page" | |||
/> | |||
<A11ySkipTarget | |||
anchor="qg_main" | |||
/> | |||
<header | |||
className="page-header" | |||
> | |||
<div | |||
className="page-title display-flex-center" | |||
> | |||
<h1> | |||
project_quality_gate.page | |||
</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div | |||
className="big-padded-top big-padded-bottom" | |||
> | |||
quality_gates.projects.help | |||
</div> | |||
} | |||
/> | |||
</div> | |||
</header> | |||
<div | |||
className="boxed-group" | |||
> | |||
<h2 | |||
className="boxed-group-header" | |||
> | |||
project_quality_gate.subtitle | |||
</h2> | |||
<form | |||
className="boxed-group-inner" | |||
onSubmit={[Function]} | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
project_quality_gate.page.description | |||
</p> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={false} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="-1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_default | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="text-muted little-spacer-right" | |||
> | |||
current_noun | |||
: | |||
</span> | |||
qualitygate | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={true} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_specific | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Select | |||
className="abs-width-300" | |||
clearable={false} | |||
disabled={false} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
] | |||
} | |||
value="1" | |||
/> | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
save | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: loading 1`] = ` | |||
<i | |||
className="spinner" | |||
/> | |||
`; | |||
exports[`should render correctly: show warning 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-quality-gate" | |||
> | |||
<Suggestions | |||
suggestions="project_quality_gate" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
title="project_quality_gate.page" | |||
/> | |||
<A11ySkipTarget | |||
anchor="qg_main" | |||
/> | |||
<header | |||
className="page-header" | |||
> | |||
<div | |||
className="page-title display-flex-center" | |||
> | |||
<h1> | |||
project_quality_gate.page | |||
</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div | |||
className="big-padded-top big-padded-bottom" | |||
> | |||
quality_gates.projects.help | |||
</div> | |||
} | |||
/> | |||
</div> | |||
</header> | |||
<div | |||
className="boxed-group" | |||
> | |||
<h2 | |||
className="boxed-group-header" | |||
> | |||
project_quality_gate.subtitle | |||
</h2> | |||
<form | |||
className="boxed-group-inner" | |||
onSubmit={[Function]} | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
project_quality_gate.page.description | |||
</p> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={false} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="-1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_default | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="text-muted little-spacer-right" | |||
> | |||
current_noun | |||
: | |||
</span> | |||
qualitygate | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={true} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="5" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_specific | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Select | |||
className="abs-width-300" | |||
clearable={false} | |||
disabled={false} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
] | |||
} | |||
value="5" | |||
/> | |||
</div> | |||
</div> | |||
</Radio> | |||
<Alert | |||
className="big-spacer-top" | |||
variant="warning" | |||
> | |||
project_quality_gate.requires_new_analysis | |||
</Alert> | |||
</div> | |||
<div> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
save | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: show warning if not using default 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-quality-gate" | |||
> | |||
<Suggestions | |||
suggestions="project_quality_gate" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
title="project_quality_gate.page" | |||
/> | |||
<A11ySkipTarget | |||
anchor="qg_main" | |||
/> | |||
<header | |||
className="page-header" | |||
> | |||
<div | |||
className="page-title display-flex-center" | |||
> | |||
<h1> | |||
project_quality_gate.page | |||
</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div | |||
className="big-padded-top big-padded-bottom" | |||
> | |||
quality_gates.projects.help | |||
</div> | |||
} | |||
/> | |||
</div> | |||
</header> | |||
<div | |||
className="boxed-group" | |||
> | |||
<h2 | |||
className="boxed-group-header" | |||
> | |||
project_quality_gate.subtitle | |||
</h2> | |||
<form | |||
className="boxed-group-inner" | |||
onSubmit={[Function]} | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
project_quality_gate.page.description | |||
</p> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={true} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="-1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_default | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="text-muted little-spacer-right" | |||
> | |||
current_noun | |||
: | |||
</span> | |||
qualitygate | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={false} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_specific | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Select | |||
className="abs-width-300" | |||
clearable={false} | |||
disabled={true} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
] | |||
} | |||
value="-1" | |||
/> | |||
</div> | |||
</div> | |||
</Radio> | |||
<Alert | |||
className="big-spacer-top" | |||
variant="warning" | |||
> | |||
project_quality_gate.requires_new_analysis | |||
</Alert> | |||
</div> | |||
<div> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
save | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: submitting 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-quality-gate" | |||
> | |||
<Suggestions | |||
suggestions="project_quality_gate" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
title="project_quality_gate.page" | |||
/> | |||
<A11ySkipTarget | |||
anchor="qg_main" | |||
/> | |||
<header | |||
className="page-header" | |||
> | |||
<div | |||
className="page-title display-flex-center" | |||
> | |||
<h1> | |||
project_quality_gate.page | |||
</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div | |||
className="big-padded-top big-padded-bottom" | |||
> | |||
quality_gates.projects.help | |||
</div> | |||
} | |||
/> | |||
</div> | |||
</header> | |||
<div | |||
className="boxed-group" | |||
> | |||
<h2 | |||
className="boxed-group-header" | |||
> | |||
project_quality_gate.subtitle | |||
</h2> | |||
<form | |||
className="boxed-group-inner" | |||
onSubmit={[Function]} | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
project_quality_gate.page.description | |||
</p> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={false} | |||
className="display-flex-start" | |||
disabled={true} | |||
onCheck={[Function]} | |||
value="-1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_default | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="text-muted little-spacer-right" | |||
> | |||
current_noun | |||
: | |||
</span> | |||
qualitygate | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={true} | |||
className="display-flex-start" | |||
disabled={true} | |||
onCheck={[Function]} | |||
value="1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_specific | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Select | |||
className="abs-width-300" | |||
clearable={false} | |||
disabled={true} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
] | |||
} | |||
value="1" | |||
/> | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
save | |||
</SubmitButton> | |||
<i | |||
className="spinner spacer-left" | |||
/> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render select options correctly: default 1`] = ` | |||
<span> | |||
Gate 1 | |||
</span> | |||
`; |
@@ -17,10 +17,5 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import Header from '../Header'; | |||
it('renders', () => { | |||
expect(shallow(<Header />)).toMatchSnapshot(); | |||
}); | |||
export const USE_SYSTEM_DEFAULT = '-1'; |
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent' | |||
const routes = [ | |||
{ | |||
indexRoute: { component: lazyLoadComponent(() => import('./App')) } | |||
indexRoute: { component: lazyLoadComponent(() => import('./ProjectQualityGateApp')) } | |||
} | |||
]; | |||
@@ -52,6 +52,8 @@ create_new_element=Create new element | |||
created=Created | |||
created_on=Created on | |||
critical=Critical | |||
current=current | |||
current_noun=Current | |||
customize=Customize | |||
date=Date | |||
days=Days | |||
@@ -1358,6 +1360,10 @@ project_quality_profile.successfully_updated={0} Quality Profile has been succes | |||
#------------------------------------------------------------------------------ | |||
project_quality_gate.default_qgate=Default | |||
project_quality_gate.successfully_updated=Quality Gate has been successfully updated. | |||
project_quality_gate.subtitle=Manage project Quality Gate | |||
project_quality_gate.always_use_default=Always use the instance default Quality Gate | |||
project_quality_gate.always_use_specific=Always use a specific Quality Gate | |||
project_quality_gate.requires_new_analysis=Changes will be applied after the next analysis. | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -1485,7 +1491,7 @@ quality_gates.conditions=Conditions | |||
quality_gates.conditions.help=Both conditions on New Code and Overall Code have to be met by a project to pass the Quality Gate. | |||
quality_gates.conditions.help.link=See also: Clean as You Code | |||
quality_gates.projects=Projects | |||
quality_gates.projects.help=The Default gate is applied to all projects not explicitly assigned to a gate. Quality Profile and Gate administrators can assign projects to a gate from the Quality Profile page. Project administrators can also choose a non-default gate. | |||
quality_gates.projects.help=The Default gate is applied to all projects not explicitly assigned to a gate. Quality Gate administrators can assign projects to a non-default gate, or always make it follow the system default. Project administrators may choose any gate. | |||
quality_gates.add_condition=Add Condition | |||
quality_gates.condition_added=Successfully added condition. | |||
quality_gates.update_condition=Update Condition |