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 };
.display-flex-start {
display: flex !important;
- align-items: flex-start;
+ align-items: flex-start !important;
}
.display-flex-end {
+++ /dev/null
-/*
- * 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>
- );
- }
-}
+++ /dev/null
-/*
- * 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>
- );
- }
-}
+++ /dev/null
-/*
- * 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>
- );
-}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
+++ /dev/null
-/*
- * 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;
-}
+++ /dev/null
-/*
- * 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}`
- };
-}
+++ /dev/null
-/*
- * 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 Header from '../Header';
-
-it('renders', () => {
- expect(shallow(<Header />)).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
+++ /dev/null
-// 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>
-`;
+++ /dev/null
-// 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>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<ProjectQualityGateAppRenderer
+ loading={true}
+ onSelect={[Function]}
+ onSubmit={[Function]}
+ selectedQualityGateId="-1"
+ submitting={false}
+/>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+/*
+ * 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.
+ */
+
+export const USE_SYSTEM_DEFAULT = '-1';
const routes = [
{
- indexRoute: { component: lazyLoadComponent(() => import('./App')) }
+ indexRoute: { component: lazyLoadComponent(() => import('./ProjectQualityGateApp')) }
}
];
created=Created
created_on=Created on
critical=Critical
+current=current
+current_noun=Current
customize=Customize
date=Date
days=Days
#------------------------------------------------------------------------------
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.
#------------------------------------------------------------------------------
#
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