@@ -0,0 +1,37 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { getJSON, post } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function getNewCodePeriod(data?: { | |||
project?: string; | |||
branch?: string; | |||
}): Promise<{ type: T.NewCodePeriodSettingType; inherited?: boolean; value?: string }> { | |||
return getJSON('/api/new_code_periods/show', data).catch(throwGlobalError); | |||
} | |||
export function setNewCodePeriod(data: { | |||
project?: string; | |||
branch?: string; | |||
type: T.NewCodePeriodSettingType; | |||
value: string | null; | |||
}): Promise<void> { | |||
return post('/api/new_code_periods/set', data).catch(throwGlobalError); | |||
} |
@@ -164,6 +164,23 @@ exports[`should work for all qualifiers 1`] = ` | |||
project_branches.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/baseline", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_baseline.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -302,6 +319,23 @@ exports[`should work for all qualifiers 2`] = ` | |||
<ul | |||
className="menu" | |||
> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/baseline", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_baseline.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -417,6 +451,35 @@ exports[`should work for all qualifiers 3`] = ` | |||
project_activity.page | |||
</Link> | |||
</li> | |||
<Dropdown | |||
data-test="administration" | |||
overlay={ | |||
<ul | |||
className="menu" | |||
> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/baseline", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_baseline.page | |||
</Link> | |||
</li> | |||
</ul> | |||
} | |||
tagName="li" | |||
> | |||
<Component /> | |||
</Dropdown> | |||
</NavBarTabs> | |||
`; | |||
@@ -515,6 +578,23 @@ exports[`should work for all qualifiers 4`] = ` | |||
<ul | |||
className="menu" | |||
> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/baseline", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_baseline.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -888,6 +968,23 @@ exports[`should work with extensions 2`] = ` | |||
project_branches.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/baseline", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_baseline.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -1040,6 +1137,23 @@ exports[`should work with multiple extensions 2`] = ` | |||
project_branches.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/baseline", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_baseline.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" |
@@ -499,6 +499,12 @@ declare namespace T { | |||
qualityGate?: string; | |||
} | |||
export type NewCodePeriodSettingType = | |||
| 'PREVIOUS_VERSION' | |||
| 'NUMBER_OF_DAYS' | |||
| 'DATE' | |||
| 'SPECIFIC_ANALYSIS'; | |||
export interface Notification { | |||
channel: string; | |||
organization?: string; |
@@ -0,0 +1,69 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 BaselineSettingDays, { Props } from '../components/BaselineSettingDays'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ isChanged: true })).toMatchSnapshot(); | |||
expect(shallowRender({ isChanged: true, isValid: false })).toMatchSnapshot(); | |||
}); | |||
it('should not display input when not selected', () => { | |||
const wrapper = shallowRender({ selected: false }); | |||
expect(wrapper.find('ValidationInput')).toHaveLength(0); | |||
}); | |||
it('should callback when clicked', () => { | |||
const onSelect = jest.fn(); | |||
const wrapper = shallowRender({ onSelect, selected: false }); | |||
wrapper | |||
.find('RadioCard') | |||
.first() | |||
.simulate('click'); | |||
expect(onSelect).toHaveBeenCalledWith('NUMBER_OF_DAYS'); | |||
}); | |||
it('should callback when changing days', () => { | |||
const onChangeDays = jest.fn(); | |||
const wrapper = shallowRender({ onChangeDays }); | |||
wrapper | |||
.find('input') | |||
.first() | |||
.simulate('change', { currentTarget: { value: '23' } }); | |||
expect(onChangeDays).toHaveBeenCalledWith('23'); | |||
}); | |||
function shallowRender(props: Partial<Props> = {}) { | |||
return shallow( | |||
<BaselineSettingDays | |||
days="28" | |||
isChanged={false} | |||
isValid={true} | |||
onChangeDays={jest.fn()} | |||
onSelect={jest.fn()} | |||
selected={true} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,46 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 BaselineSettingPreviousVersion, { | |||
Props | |||
} from '../components/BaselineSettingPreviousVersion'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ isDefault: true })).toMatchSnapshot(); | |||
}); | |||
it('should callback when clicked', () => { | |||
const onSelect = jest.fn(); | |||
const wrapper = shallowRender({ onSelect, selected: false }); | |||
wrapper | |||
.find('RadioCard') | |||
.first() | |||
.simulate('click'); | |||
expect(onSelect).toHaveBeenCalledWith('PREVIOUS_VERSION'); | |||
}); | |||
function shallowRender(props: Partial<Props> = {}) { | |||
return shallow( | |||
<BaselineSettingPreviousVersion onSelect={jest.fn()} selected={true} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,82 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<RadioCard | |||
onClick={[Function]} | |||
selected={true} | |||
title="baseline.number_days" | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
baseline.number_days.description | |||
</p> | |||
<ValidationInput | |||
id="baseline_number_of_days" | |||
isInvalid={false} | |||
isValid={false} | |||
label="baseline.specify_days" | |||
required={true} | |||
> | |||
<input | |||
onChange={[Function]} | |||
type="text" | |||
value="28" | |||
/> | |||
</ValidationInput> | |||
</RadioCard> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<RadioCard | |||
onClick={[Function]} | |||
selected={true} | |||
title="baseline.number_days" | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
baseline.number_days.description | |||
</p> | |||
<ValidationInput | |||
id="baseline_number_of_days" | |||
isInvalid={false} | |||
isValid={true} | |||
label="baseline.specify_days" | |||
required={true} | |||
> | |||
<input | |||
onChange={[Function]} | |||
type="text" | |||
value="28" | |||
/> | |||
</ValidationInput> | |||
</RadioCard> | |||
`; | |||
exports[`should render correctly 3`] = ` | |||
<RadioCard | |||
onClick={[Function]} | |||
selected={true} | |||
title="baseline.number_days" | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
baseline.number_days.description | |||
</p> | |||
<ValidationInput | |||
id="baseline_number_of_days" | |||
isInvalid={true} | |||
isValid={false} | |||
label="baseline.specify_days" | |||
required={true} | |||
> | |||
<input | |||
onChange={[Function]} | |||
type="text" | |||
value="28" | |||
/> | |||
</ValidationInput> | |||
</RadioCard> | |||
`; |
@@ -0,0 +1,25 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<RadioCard | |||
onClick={[Function]} | |||
selected={true} | |||
title="baseline.previous_version" | |||
> | |||
<p> | |||
baseline.previous_version.description | |||
</p> | |||
</RadioCard> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<RadioCard | |||
onClick={[Function]} | |||
selected={true} | |||
title="baseline.previous_version (default)" | |||
> | |||
<p> | |||
baseline.previous_version.description | |||
</p> | |||
</RadioCard> | |||
`; |
@@ -0,0 +1,59 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 RadioCard from 'sonar-ui-common/components/controls/RadioCard'; | |||
import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
export interface Props { | |||
className?: string; | |||
days: string; | |||
isChanged: boolean; | |||
isValid: boolean; | |||
onChangeDays: (value: string) => void; | |||
onSelect: (selection: T.NewCodePeriodSettingType) => void; | |||
selected: boolean; | |||
} | |||
export default function BaselineSettingDays(props: Props) { | |||
const { className, days, isChanged, isValid, onChangeDays, onSelect, selected } = props; | |||
return ( | |||
<RadioCard | |||
className={className} | |||
onClick={() => onSelect('NUMBER_OF_DAYS')} | |||
selected={selected} | |||
title={translate('baseline.number_days')}> | |||
<> | |||
<p className="big-spacer-bottom">{translate('baseline.number_days.description')}</p> | |||
{selected && ( | |||
<ValidationInput | |||
error={undefined} | |||
id="baseline_number_of_days" | |||
isInvalid={isChanged && !isValid} | |||
isValid={isChanged && isValid} | |||
label={translate('baseline.specify_days')} | |||
required={true}> | |||
<input onChange={e => onChangeDays(e.currentTarget.value)} type="text" value={days} /> | |||
</ValidationInput> | |||
)} | |||
</> | |||
</RadioCard> | |||
); | |||
} |
@@ -0,0 +1,41 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 RadioCard from 'sonar-ui-common/components/controls/RadioCard'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
export interface Props { | |||
isDefault?: boolean; | |||
onSelect: (selection: T.NewCodePeriodSettingType) => void; | |||
selected: boolean; | |||
} | |||
export default function BaselineSettingPreviousVersion({ isDefault, onSelect, selected }: Props) { | |||
return ( | |||
<RadioCard | |||
onClick={() => onSelect('PREVIOUS_VERSION')} | |||
selected={selected} | |||
title={ | |||
translate('baseline.previous_version') + (isDefault ? ` (${translate('default')})` : '') | |||
}> | |||
<p>{translate('baseline.previous_version.description')}</p> | |||
</RadioCard> | |||
); | |||
} |
@@ -22,6 +22,7 @@ import { sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { IndexLink } from 'react-router'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getSettingsAppAllCategories, Store } from '../../../store/rootReducer'; | |||
import { getCategoryName } from '../utils'; | |||
@@ -37,6 +38,10 @@ interface Props { | |||
selectedCategory: string; | |||
} | |||
const FIXED_CATEGORIES = [ | |||
{ key: 'new_code_period', name: translate('settings.new_code_period.category') } | |||
]; | |||
export class CategoriesList extends React.PureComponent<Props> { | |||
renderLink(category: Category) { | |||
const { component, defaultCategory, selectedCategory } = this.props; | |||
@@ -58,10 +63,12 @@ export class CategoriesList extends React.PureComponent<Props> { | |||
} | |||
render() { | |||
const categoriesWithName = this.props.categories.map(key => ({ | |||
key, | |||
name: getCategoryName(key) | |||
})); | |||
const categoriesWithName = this.props.categories | |||
.map(key => ({ | |||
key, | |||
name: getCategoryName(key) | |||
})) | |||
.concat(FIXED_CATEGORIES); | |||
const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase()); | |||
return ( | |||
<ul className="side-tabs-menu"> |
@@ -29,6 +29,7 @@ import { fetchSettings } from '../store/actions'; | |||
import '../styles.css'; | |||
import AllCategoriesList from './AllCategoriesList'; | |||
import CategoryDefinitionsList from './CategoryDefinitionsList'; | |||
import NewCodePeriod from './NewCodePeriod'; | |||
import PageHeader from './PageHeader'; | |||
import WildcardsHelp from './WildcardsHelp'; | |||
@@ -96,7 +97,15 @@ export class App extends React.PureComponent<Props & WithRouterProps, State> { | |||
/> | |||
</div> | |||
<div className="side-tabs-main"> | |||
<CategoryDefinitionsList category={selectedCategory} component={this.props.component} /> | |||
{selectedCategory === 'new_code_period' ? ( | |||
<NewCodePeriod /> | |||
) : ( | |||
<CategoryDefinitionsList | |||
category={selectedCategory} | |||
component={this.props.component} | |||
/> | |||
)} | |||
{selectedCategory === 'exclusions' && <WildcardsHelp />} | |||
</div> | |||
</div> |
@@ -0,0 +1,216 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { FormattedMessage } from 'react-intl'; | |||
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod'; | |||
import BaselineSettingDays from '../../projectBaseline/components/BaselineSettingDays'; | |||
import BaselineSettingPreviousVersion from '../../projectBaseline/components/BaselineSettingPreviousVersion'; | |||
import { validateDays } from '../../projectBaseline/utils'; | |||
interface Props {} | |||
interface State { | |||
currentSetting?: T.NewCodePeriodSettingType; | |||
days: string; | |||
loading: boolean; | |||
currentSettingValue?: string | number; | |||
saving: boolean; | |||
selected?: T.NewCodePeriodSettingType; | |||
success: boolean; | |||
} | |||
const DEFAULT_SETTING = 'PREVIOUS_VERSION'; | |||
export default class NewCodePeriod extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
loading: true, | |||
days: '30', | |||
saving: false, | |||
success: false | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchNewCodePeriodSetting(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchNewCodePeriodSetting() { | |||
getNewCodePeriod() | |||
.then(({ type, value }) => { | |||
const currentSetting = type || DEFAULT_SETTING; | |||
this.setState(({ days }) => ({ | |||
currentSetting, | |||
days: currentSetting === 'NUMBER_OF_DAYS' ? String(value) : days, | |||
loading: false, | |||
currentSettingValue: value, | |||
selected: currentSetting | |||
})); | |||
}) | |||
.catch(() => { | |||
this.setState({ loading: false }); | |||
}); | |||
} | |||
onSelectDays = (days: string) => { | |||
this.setState({ days, success: false }); | |||
}; | |||
onSelectSetting = (selected: T.NewCodePeriodSettingType) => { | |||
this.setState({ selected, success: false }); | |||
}; | |||
onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => { | |||
e.preventDefault(); | |||
const { days, selected } = this.state; | |||
const type = selected; | |||
const value = type === 'NUMBER_OF_DAYS' ? days : null; | |||
if (type) { | |||
this.setState({ saving: true, success: false }); | |||
setNewCodePeriod({ | |||
type, | |||
value | |||
}).then( | |||
() => { | |||
this.setState({ | |||
saving: false, | |||
currentSetting: type, | |||
currentSettingValue: value || undefined, | |||
success: true | |||
}); | |||
}, | |||
() => { | |||
this.setState({ | |||
saving: false | |||
}); | |||
} | |||
); | |||
} | |||
}; | |||
render() { | |||
const { | |||
currentSetting, | |||
days, | |||
loading, | |||
currentSettingValue, | |||
saving, | |||
selected, | |||
success | |||
} = this.state; | |||
const isChanged = | |||
selected !== currentSetting || | |||
(selected === 'NUMBER_OF_DAYS' && String(days) !== currentSettingValue); | |||
const isValid = selected !== 'NUMBER_OF_DAYS' || validateDays(days); | |||
return ( | |||
<ul className="settings-sub-categories-list"> | |||
<li> | |||
<ul className="settings-definitions-list"> | |||
<li> | |||
<div className="settings-definition"> | |||
<div className="settings-definition-left"> | |||
<h3 | |||
className="settings-definition-name" | |||
title={translate('settings.new_code_period.title')}> | |||
{translate('settings.new_code_period.title')} | |||
</h3> | |||
<div className="small big-spacer-top"> | |||
<FormattedMessage | |||
defaultMessage={translate('settings.new_code_period.description')} | |||
id="settings.new_code_period.description" | |||
values={{ | |||
link: ( | |||
<a href="/documentation/user-guide/fixing-the-water-leak/"> | |||
{translate('learn_more')} | |||
</a> | |||
) | |||
}} | |||
/> | |||
<p className="spacer-top"> | |||
{translate('settings.new_code_period.description2')} | |||
</p> | |||
</div> | |||
</div> | |||
<div className="settings-definition-right"> | |||
{loading ? ( | |||
<DeferredSpinner /> | |||
) : ( | |||
<form onSubmit={this.onSubmit}> | |||
<BaselineSettingPreviousVersion | |||
isDefault={true} | |||
onSelect={this.onSelectSetting} | |||
selected={selected === 'PREVIOUS_VERSION'} | |||
/> | |||
<BaselineSettingDays | |||
className="spacer-top" | |||
days={days} | |||
isChanged={isChanged} | |||
isValid={isValid} | |||
onChangeDays={this.onSelectDays} | |||
onSelect={this.onSelectSetting} | |||
selected={selected === 'NUMBER_OF_DAYS'} | |||
/> | |||
{isChanged && ( | |||
<div className="big-spacer-top"> | |||
<p className="spacer-bottom"> | |||
{translate('baseline.next_analysis_notice')} | |||
</p> | |||
<DeferredSpinner className="spacer-right" loading={saving} /> | |||
<SubmitButton disabled={saving || !isValid}> | |||
{translate('save')} | |||
</SubmitButton> | |||
</div> | |||
)} | |||
{!saving && !loading && success && ( | |||
<div className="big-spacer-top"> | |||
<span className="text-success"> | |||
<AlertSuccessIcon className="spacer-right" /> | |||
{translate('settings.state.saved')} | |||
</span> | |||
</div> | |||
)} | |||
</form> | |||
)} | |||
</div> | |||
</div> | |||
</li> | |||
</ul> | |||
</li> | |||
</ul> | |||
); | |||
} | |||
} |
@@ -0,0 +1,41 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { CategoriesList } from '../AllCategoriesList'; | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('li')).toHaveLength(3); | |||
}); | |||
function shallowRender(props: Partial<CategoriesList['props']> = {}) { | |||
const categories = ['COBOL', 'general']; | |||
return shallow( | |||
<CategoriesList | |||
categories={categories} | |||
defaultCategory="general" | |||
selectedCategory="" | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,49 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { App } from '../AppContainer'; | |||
import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render newCodePeriod correctly', () => { | |||
const wrapper = shallowRender({ | |||
location: mockLocation({ query: { category: 'new_code_period' } }) | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<App['props']> = {}) { | |||
return shallow( | |||
<App | |||
defaultCategory="general" | |||
fetchSettings={jest.fn().mockResolvedValue({})} | |||
location={mockLocation()} | |||
params={{}} | |||
router={mockRouter()} | |||
routes={[]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,136 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { getNewCodePeriod, setNewCodePeriod } from '../../../../api/newCodePeriod'; | |||
import NewCodePeriod from '../NewCodePeriod'; | |||
jest.mock('../../../../api/newCodePeriod', () => ({ | |||
getNewCodePeriod: jest.fn().mockResolvedValue({}), | |||
setNewCodePeriod: jest.fn(() => Promise.resolve()) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should load the current new code period on mount', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(getNewCodePeriod).toHaveBeenCalledTimes(1); | |||
expect(wrapper.state('currentSetting')).toBe('PREVIOUS_VERSION'); | |||
}); | |||
it('should load the current new code period with value on mount', async () => { | |||
(getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '42' }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(getNewCodePeriod).toHaveBeenCalledTimes(1); | |||
expect(wrapper.state('currentSetting')).toBe('NUMBER_OF_DAYS'); | |||
expect(wrapper.state('days')).toBe('42'); | |||
}); | |||
it('should only show the save button after changing the setting', async () => { | |||
(getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'PREVIOUS_VERSION' }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('selected')).toBe('PREVIOUS_VERSION'); | |||
expect(wrapper.find('SubmitButton')).toHaveLength(0); | |||
wrapper.instance().onSelectSetting('NUMBER_OF_DAYS'); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.find('SubmitButton')).toHaveLength(1); | |||
}); | |||
it('should disable the button if the days are invalid', async () => { | |||
(getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '42' }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().onSelectDays('asd'); | |||
await waitAndUpdate(wrapper); | |||
expect( | |||
wrapper | |||
.find('SubmitButton') | |||
.first() | |||
.prop('disabled') | |||
).toBe(true); | |||
wrapper.instance().onSelectDays('23'); | |||
await waitAndUpdate(wrapper); | |||
expect( | |||
wrapper | |||
.find('SubmitButton') | |||
.first() | |||
.prop('disabled') | |||
).toBe(false); | |||
}); | |||
it('should submit correctly', async () => { | |||
(getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '42' }); | |||
const preventDefault = jest.fn(); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().onSelectSetting('PREVIOUS_VERSION'); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('form').simulate('submit', { preventDefault }); | |||
expect(preventDefault).toBeCalledTimes(1); | |||
expect(setNewCodePeriod).toBeCalledWith({ type: 'PREVIOUS_VERSION', value: null }); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected')); | |||
}); | |||
it('should submit correctly with days', async () => { | |||
(getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '42' }); | |||
const preventDefault = jest.fn(); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().onSelectDays('66'); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('form').simulate('submit', { preventDefault }); | |||
expect(preventDefault).toBeCalledTimes(1); | |||
expect(setNewCodePeriod).toBeCalledWith({ type: 'NUMBER_OF_DAYS', value: '66' }); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected')); | |||
}); | |||
function shallowRender() { | |||
return shallow<NewCodePeriod>(<NewCodePeriod />); | |||
} |
@@ -0,0 +1,65 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<ul | |||
className="side-tabs-menu" | |||
> | |||
<li | |||
key="COBOL" | |||
> | |||
<IndexLink | |||
className="" | |||
title="COBOL" | |||
to={ | |||
Object { | |||
"pathname": "/settings", | |||
"query": Object { | |||
"category": "cobol", | |||
"id": undefined, | |||
}, | |||
} | |||
} | |||
> | |||
COBOL | |||
</IndexLink> | |||
</li> | |||
<li | |||
key="general" | |||
> | |||
<IndexLink | |||
className="" | |||
title="general" | |||
to={ | |||
Object { | |||
"pathname": "/settings", | |||
"query": Object { | |||
"category": undefined, | |||
"id": undefined, | |||
}, | |||
} | |||
} | |||
> | |||
general | |||
</IndexLink> | |||
</li> | |||
<li | |||
key="new_code_period" | |||
> | |||
<IndexLink | |||
className="" | |||
title="settings.new_code_period.category" | |||
to={ | |||
Object { | |||
"pathname": "/settings", | |||
"query": Object { | |||
"category": "new_code_period", | |||
"id": undefined, | |||
}, | |||
} | |||
} | |||
> | |||
settings.new_code_period.category | |||
</IndexLink> | |||
</li> | |||
</ul> | |||
`; |
@@ -0,0 +1,5 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = `""`; | |||
exports[`should render newCodePeriod correctly 1`] = `""`; |
@@ -0,0 +1,59 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<ul | |||
className="settings-sub-categories-list" | |||
> | |||
<li> | |||
<ul | |||
className="settings-definitions-list" | |||
> | |||
<li> | |||
<div | |||
className="settings-definition" | |||
> | |||
<div | |||
className="settings-definition-left" | |||
> | |||
<h3 | |||
className="settings-definition-name" | |||
title="settings.new_code_period.title" | |||
> | |||
settings.new_code_period.title | |||
</h3> | |||
<div | |||
className="small big-spacer-top" | |||
> | |||
<FormattedMessage | |||
defaultMessage="settings.new_code_period.description" | |||
id="settings.new_code_period.description" | |||
values={ | |||
Object { | |||
"link": <a | |||
href="/documentation/user-guide/fixing-the-water-leak/" | |||
> | |||
learn_more | |||
</a>, | |||
} | |||
} | |||
/> | |||
<p | |||
className="spacer-top" | |||
> | |||
settings.new_code_period.description2 | |||
</p> | |||
</div> | |||
</div> | |||
<div | |||
className="settings-definition-right" | |||
> | |||
<DeferredSpinner | |||
timeout={100} | |||
/> | |||
</div> | |||
</div> | |||
</li> | |||
</ul> | |||
</li> | |||
</ul> | |||
`; |
@@ -538,6 +538,38 @@ project_branches.page=Branches & Pull Requests | |||
project_branches.page.description=Use this page to manage project branches and pull requests. | |||
project_branches.page.life_time=Short-lived branches and pull requests are permanently deleted after {days} days without analysis. | |||
project_branches.page.life_time.admin=You can adjust this value globally in {settings}. | |||
project_baseline.page=New Code Period | |||
project_baseline.page.description=Use this page to manage the New Code Period of your project. {link} | |||
project_baseline.page.description.link=Learn More | |||
project_baseline.page.description2=You can adjust this setting globally in {link} | |||
project_baseline.page.description2.link=General Settings | |||
project_baseline.default_setting=Project default setting | |||
project_baseline.default_setting.description=This setting is the default for all branches of the project | |||
baseline.previous_version=Previous version | |||
baseline.previous_version.description=The New Code Period will begin with the analysis following the previous version. | |||
baseline.number_days=Number of days | |||
baseline.number_days.description=Specify a number of days to set a floating New Code Period window. | |||
baseline.specific_date=Specific date | |||
baseline.specific_date.description=Set a specific date as the start of the New Code Period. (First analysis on this date will be used) | |||
baseline.specific_analysis=Specific analysis | |||
baseline.specific_analysis.description=Choose an analysis as the baseline for the New Code Period. | |||
baseline.specify_days=Specify a number of days | |||
baseline.last_analysis_before=Last analysis before | |||
baseline.next_analysis_notice=Changes will take effect after the next analysis | |||
branch_list.branch=Branch | |||
branch_list.current_setting=Current setting | |||
branch_list.current_baseline=Current Baseline | |||
branch_list.edit_settings=Edit settings | |||
baseline.new_code_period_for_branch_x=New Code Period for {0} | |||
baseline.analysis_from=Analysis from: | |||
baseline.branch_analyses.ranges.30days=Last 30 days | |||
baseline.branch_analyses.ranges.allTime=All time | |||
baseline.no_analyses=No analyses | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -880,6 +912,12 @@ settings.wildcards.single_char=Match a single character | |||
settings.wildcards.matches=Matches | |||
settings.wildcards.does_no_match=Does not match | |||
settings.new_code_period.category=New Code Period | |||
settings.new_code_period.title=Default New Code Period behavior | |||
settings.new_code_period.description=The New Code Period is the period used to compare measures and track new issues. {link} | |||
settings.new_code_period.description2=This setting is the default for all projects. A specific New Code Period setting can be configured at project level. | |||
property.category.general=General | |||
property.category.general.email=Email | |||
property.category.general.duplications=Duplications |