瀏覽代碼

SONAR-13856 Add 'Always use the Default' option at project level for QG

tags/8.6.0.39681
Wouter Admiraal 3 年之前
父節點
當前提交
6ecb6c4084
共有 18 個檔案被更改,包括 1412 行新增571 行删除
  1. 1
    1
      server/sonar-web/src/main/js/api/quality-gates.ts
  2. 1
    1
      server/sonar-web/src/main/js/app/styles/init/misc.css
  3. 0
    147
      server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx
  4. 0
    117
      server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx
  5. 0
    41
      server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx
  6. 192
    0
      server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx
  7. 166
    0
      server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
  8. 0
    145
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx
  9. 0
    48
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx
  10. 178
    0
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx
  11. 111
    0
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx
  12. 0
    33
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap
  13. 0
    30
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap
  14. 11
    0
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap
  15. 743
    0
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap
  16. 1
    6
      server/sonar-web/src/main/js/apps/projectQualityGate/constants.ts
  17. 1
    1
      server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts
  18. 7
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-web/src/main/js/api/quality-gates.ts 查看文件

@@ -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 };

+ 1
- 1
server/sonar-web/src/main/js/app/styles/init/misc.css 查看文件

@@ -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 {

+ 0
- 147
server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx 查看文件

@@ -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>
);
}
}

+ 0
- 117
server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx 查看文件

@@ -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>
);
}
}

+ 0
- 41
server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx 查看文件

@@ -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>
);
}

+ 192
- 0
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx 查看文件

@@ -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}
/>
);
}
}

+ 166
- 0
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx 查看文件

@@ -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>
);
}

+ 0
- 145
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx 查看文件

@@ -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;
}

+ 0
- 48
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx 查看文件

@@ -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}`
};
}

+ 178
- 0
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx 查看文件

@@ -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}
/>
);
}

+ 111
- 0
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx 查看文件

@@ -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}
/>
);
}

+ 0
- 33
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap 查看文件

@@ -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>
`;

+ 0
- 30
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap 查看文件

@@ -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>
`;

+ 11
- 0
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap 查看文件

@@ -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}
/>
`;

+ 743
- 0
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap 查看文件

@@ -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>
`;

server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx → server/sonar-web/src/main/js/apps/projectQualityGate/constants.ts 查看文件

@@ -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';

+ 1
- 1
server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts 查看文件

@@ -21,7 +21,7 @@ import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent'

const routes = [
{
indexRoute: { component: lazyLoadComponent(() => import('./App')) }
indexRoute: { component: lazyLoadComponent(() => import('./ProjectQualityGateApp')) }
}
];


+ 7
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -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

Loading…
取消
儲存