@@ -20,12 +20,26 @@ | |||
import { omitBy } from 'lodash'; | |||
import { getJSON, RequestData, post, postJSON } from '../helpers/request'; | |||
import { TYPE_PROPERTY_SET } from '../apps/settings/constants'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function getDefinitions(component: string | null, branch?: string): Promise<any> { | |||
return getJSON('/api/settings/list_definitions', { branch, component }).then(r => r.definitions); | |||
} | |||
export function getValues(keys: string, component?: string, branch?: string): Promise<any> { | |||
export interface SettingValue { | |||
inherited?: boolean; | |||
key: string; | |||
parentValue?: string; | |||
parentValues?: string[]; | |||
value?: any; | |||
values?: string[]; | |||
} | |||
export function getValues( | |||
keys: string, | |||
component?: string, | |||
branch?: string | |||
): Promise<SettingValue[]> { | |||
return getJSON('/api/settings/values', { keys, component, branch }).then(r => r.settings); | |||
} | |||
@@ -51,6 +65,15 @@ export function setSettingValue( | |||
return post('/api/settings/set', data); | |||
} | |||
export function setSimpleSettingValue(parameters: { | |||
branch?: string; | |||
component?: string; | |||
value: string; | |||
key: string; | |||
}): Promise<void | Response> { | |||
return post('/api/settings/set', parameters).catch(throwGlobalError); | |||
} | |||
export function resetSettingValue(key: string, component?: string, branch?: string): Promise<void> { | |||
return post('/api/settings/reset', { keys: key, component, branch }); | |||
} |
@@ -196,7 +196,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
renderAdministration() { | |||
const { branch } = this.props; | |||
if (!this.getConfiguration().showSettings || (branch && isShortLivingBranch(branch))) { | |||
if (!this.getConfiguration().showSettings || (branch && !branch.isMain)) { | |||
return null; | |||
} | |||
@@ -739,25 +739,6 @@ exports[`should work for long-living branches 1`] = ` | |||
project_activity.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
className="is-admin" | |||
id="component-navigation-admin" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/settings", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
branches.branch_settings | |||
</Link> | |||
</li> | |||
</NavBarTabs> | |||
`; | |||
@@ -23,6 +23,7 @@ export enum BranchType { | |||
} | |||
export interface MainBranch { | |||
analysisDate?: string; | |||
isMain: true; | |||
name: string; | |||
status?: { | |||
@@ -31,6 +32,7 @@ export interface MainBranch { | |||
} | |||
export interface LongLivingBranch { | |||
analysisDate?: string; | |||
isMain: false; | |||
name: string; | |||
status?: { | |||
@@ -40,6 +42,7 @@ export interface LongLivingBranch { | |||
} | |||
export interface ShortLivingBranch { | |||
analysisDate?: string; | |||
isMain: false; | |||
isOrphan?: true; | |||
mergeBranch: string; |
@@ -24,6 +24,7 @@ import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import RemoveMemberForm from './forms/RemoveMemberForm'; | |||
import ManageMemberGroupsForm from './forms/ManageMemberGroupsForm'; | |||
import SettingsIcon from '../../../components/icons-components/SettingsIcon'; | |||
/*:: import type { Member } from '../../../store/organizationsMembers/actions'; */ | |||
/*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */ | |||
@@ -67,7 +68,7 @@ export default class MembersListItem extends React.PureComponent { | |||
<button | |||
className="dropdown-toggle little-spacer-right button-compact" | |||
data-toggle="dropdown"> | |||
<i className="icon-settings" /> <i className="icon-dropdown" /> | |||
<SettingsIcon style={{ marginTop: 4 }} /> <i className="icon-dropdown" /> | |||
</button> | |||
<ul className="dropdown-menu dropdown-menu-right"> | |||
<li> |
@@ -38,8 +38,12 @@ exports[`should groups at 0 if the groupCount field is not defined (just added u | |||
className="dropdown-toggle little-spacer-right button-compact" | |||
data-toggle="dropdown" | |||
> | |||
<i | |||
className="icon-settings" | |||
<SettingsIcon | |||
style={ | |||
Object { | |||
"marginTop": 4, | |||
} | |||
} | |||
/> | |||
<i | |||
@@ -159,8 +163,12 @@ exports[`should render actions and groups for admin 1`] = ` | |||
className="dropdown-toggle little-spacer-right button-compact" | |||
data-toggle="dropdown" | |||
> | |||
<i | |||
className="icon-settings" | |||
<SettingsIcon | |||
style={ | |||
Object { | |||
"marginTop": 4, | |||
} | |||
} | |||
/> | |||
<i |
@@ -24,6 +24,7 @@ import Events from './Events'; | |||
import AddEventForm from './forms/AddEventForm'; | |||
import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; | |||
import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter'; | |||
import SettingsIcon from '../../../components/icons-components/SettingsIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: import type { Analysis } from '../types'; */ | |||
@@ -76,7 +77,7 @@ export default class ProjectActivityAnalysis extends React.PureComponent { | |||
className="js-analysis-actions button-small button-compact dropdown-toggle" | |||
data-toggle="dropdown" | |||
onClick={this.stopPropagation}> | |||
<i className="icon-settings" /> <i className="icon-dropdown" /> | |||
<SettingsIcon size={12} style={{ marginTop: 3 }} /> <i className="icon-dropdown" /> | |||
</button> | |||
<ul className="dropdown-menu dropdown-menu-right"> | |||
{!hasVersion && |
@@ -19,42 +19,129 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import BranchRow from './BranchRow'; | |||
import LongBranchesPattern from './LongBranchesPattern'; | |||
import { Branch } from '../../../app/types'; | |||
import { sortBranchesAsTree } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getValues } from '../../../api/settings'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { Link } from 'react-router'; | |||
interface Props { | |||
branches: Branch[]; | |||
canAdmin?: boolean; | |||
component: { key: string }; | |||
onBranchesChange: () => void; | |||
} | |||
export default function App({ branches, component, onBranchesChange }: Props) { | |||
return ( | |||
<div className="page page-limited"> | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('project_branches.page')}</h1> | |||
</header> | |||
<table className="data zebra zebra-hover"> | |||
<thead> | |||
<tr> | |||
<th>{translate('branch')}</th> | |||
<th className="text-right">{translate('status')}</th> | |||
<th className="text-right">{translate('actions')}</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{sortBranchesAsTree(branches).map(branch => ( | |||
<BranchRow | |||
branch={branch} | |||
component={component.key} | |||
key={branch.name} | |||
onChange={onBranchesChange} | |||
/> | |||
))} | |||
</tbody> | |||
</table> | |||
</div> | |||
); | |||
interface State { | |||
branchLifeTime?: string; | |||
loading: boolean; | |||
} | |||
const BRANCH_LIFETIME_SETTING = 'sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches'; | |||
export default class App extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchPurgeSetting(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchPurgeSetting() { | |||
this.setState({ loading: true }); | |||
getValues(BRANCH_LIFETIME_SETTING).then( | |||
settings => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
branchLifeTime: settings.length > 0 ? settings[0].value : undefined | |||
}); | |||
} | |||
}, | |||
() => { | |||
this.setState({ loading: false }); | |||
} | |||
); | |||
} | |||
renderBranchLifeTime() { | |||
const { branchLifeTime } = this.state; | |||
if (!branchLifeTime) { | |||
return null; | |||
} | |||
const messageKey = this.props.canAdmin | |||
? 'project_branches.page.life_time.admin' | |||
: 'project_branches.page.life_time'; | |||
return ( | |||
<p className="page-description"> | |||
<FormattedMessage | |||
defaultMessage={translate(messageKey)} | |||
id={messageKey} | |||
values={{ | |||
days: formatMeasure(this.state.branchLifeTime, 'INT'), | |||
settings: <Link to="/admin/settings">{translate('settings.page')}</Link> | |||
}} | |||
/> | |||
</p> | |||
); | |||
} | |||
render() { | |||
const { branches, component, onBranchesChange } = this.props; | |||
if (this.state.loading) { | |||
return ( | |||
<div className="page page-limited"> | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('project_branches.page')}</h1> | |||
</header> | |||
<i className="spinner" /> | |||
</div> | |||
); | |||
} | |||
return ( | |||
<div className="page page-limited"> | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('project_branches.page')}</h1> | |||
<LongBranchesPattern project={component.key} /> | |||
<p className="page-description">{translate('project_branches.page.description')}</p> | |||
{this.renderBranchLifeTime()} | |||
</header> | |||
<table className="data zebra zebra-hover"> | |||
<thead> | |||
<tr> | |||
<th>{translate('branch')}</th> | |||
<th className="thin nowrap text-right">{translate('status')}</th> | |||
<th className="thin nowrap text-right"> | |||
{translate('project_history.last_snapshot')} | |||
</th> | |||
<th className="thin nowrap text-right">{translate('actions')}</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{sortBranchesAsTree(branches).map(branch => ( | |||
<BranchRow | |||
branch={branch} | |||
component={component.key} | |||
key={branch.name} | |||
onChange={onBranchesChange} | |||
/> | |||
))} | |||
</tbody> | |||
</table> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import App from './App'; | |||
import { getAppState } from '../../../store/rootReducer'; | |||
const mapStateToProps = (state: any) => ({ | |||
canAdmin: getAppState(state).canAdmin | |||
}); | |||
export default connect<any, any, any>(mapStateToProps)(App); |
@@ -21,14 +21,14 @@ import * as React from 'react'; | |||
import { Branch } from '../../../app/types'; | |||
import * as classNames from 'classnames'; | |||
import DeleteBranchModal from './DeleteBranchModal'; | |||
import LeakPeriodForm from './LeakPeriodForm'; | |||
import BranchStatus from '../../../components/common/BranchStatus'; | |||
import BranchIcon from '../../../components/icons-components/BranchIcon'; | |||
import { isShortLivingBranch } from '../../../helpers/branches'; | |||
import ChangeIcon from '../../../components/icons-components/ChangeIcon'; | |||
import DeleteIcon from '../../../components/icons-components/DeleteIcon'; | |||
import { isShortLivingBranch, isLongLivingBranch } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import RenameBranchModal from './RenameBranchModal'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import SettingsIcon from '../../../components/icons-components/SettingsIcon'; | |||
interface Props { | |||
branch: Branch; | |||
@@ -37,13 +37,14 @@ interface Props { | |||
} | |||
interface State { | |||
changingLeak: boolean; | |||
deleting: boolean; | |||
renaming: boolean; | |||
} | |||
export default class BranchRow extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { deleting: false, renaming: false }; | |||
state: State = { changingLeak: false, deleting: false, renaming: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -80,6 +81,18 @@ export default class BranchRow extends React.PureComponent<Props, State> { | |||
this.setState({ renaming: false }); | |||
}; | |||
handleChangeLeakClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.setState({ changingLeak: true }); | |||
}; | |||
handleChangingLeakStop = () => { | |||
if (this.mounted) { | |||
this.setState({ changingLeak: false }); | |||
} | |||
}; | |||
render() { | |||
const { branch, component } = this.props; | |||
@@ -101,19 +114,47 @@ export default class BranchRow extends React.PureComponent<Props, State> { | |||
<BranchStatus branch={branch} /> | |||
</td> | |||
<td className="thin nowrap text-right"> | |||
{branch.isMain ? ( | |||
<Tooltip overlay={translate('branches.rename')}> | |||
<a className="js-rename link-no-underline" href="#" onClick={this.handleRenameClick}> | |||
<ChangeIcon /> | |||
</a> | |||
</Tooltip> | |||
) : ( | |||
<Tooltip overlay={translate('branches.delete')}> | |||
<a className="js-delete link-no-underline" href="#" onClick={this.handleDeleteClick}> | |||
<DeleteIcon /> | |||
</a> | |||
</Tooltip> | |||
)} | |||
{branch.analysisDate && <DateFromNow date={branch.analysisDate} />} | |||
</td> | |||
<td className="thin nowrap text-right"> | |||
<div className="dropdown big-spacer-left"> | |||
<button | |||
className="dropdown-toggle little-spacer-right button-compact" | |||
data-toggle="dropdown"> | |||
<SettingsIcon style={{ marginTop: 4 }} /> <i className="icon-dropdown" /> | |||
</button> | |||
<ul className="dropdown-menu dropdown-menu-right"> | |||
{isLongLivingBranch(branch) && ( | |||
<li> | |||
<a | |||
className="js-change-leak-period link-no-underline" | |||
href="#" | |||
onClick={this.handleChangeLeakClick}> | |||
{translate('branches.set_leak_period')} | |||
</a> | |||
</li> | |||
)} | |||
{branch.isMain ? ( | |||
<li> | |||
<a | |||
className="js-rename link-no-underline" | |||
href="#" | |||
onClick={this.handleRenameClick}> | |||
{translate('branches.rename')} | |||
</a> | |||
</li> | |||
) : ( | |||
<li> | |||
<a | |||
className="js-delete link-no-underline" | |||
href="#" | |||
onClick={this.handleDeleteClick}> | |||
{translate('branches.delete')} | |||
</a> | |||
</li> | |||
)} | |||
</ul> | |||
</div> | |||
</td> | |||
{this.state.deleting && ( | |||
@@ -133,6 +174,14 @@ export default class BranchRow extends React.PureComponent<Props, State> { | |||
onRename={this.handleChange} | |||
/> | |||
)} | |||
{this.state.changingLeak && ( | |||
<LeakPeriodForm | |||
branch={branch.name} | |||
onClose={this.handleChangingLeakStop} | |||
project={component} | |||
/> | |||
)} | |||
</tr> | |||
); | |||
} |
@@ -0,0 +1,106 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 Modal from 'react-modal'; | |||
import SettingForm from './SettingForm'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getValues, SettingValue } from '../../../api/settings'; | |||
interface Props { | |||
branch: string; | |||
onClose: () => void; | |||
project: string; | |||
} | |||
interface State { | |||
loading: boolean; | |||
setting?: SettingValue; | |||
submitting: boolean; | |||
value?: string; | |||
} | |||
const LEAK_PERIOD = 'sonar.leak.period'; | |||
export default class LeakPeriodForm extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { loading: true, submitting: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchSetting(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchSetting() { | |||
this.setState({ loading: true }); | |||
getValues(LEAK_PERIOD, this.props.project, this.props.branch).then( | |||
settings => { | |||
if (this.mounted) { | |||
this.setState({ loading: false, setting: settings[0] }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
} | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
}; | |||
render() { | |||
const { setting } = this.state; | |||
const header = translate('branches.set_leak_period'); | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
contentLabel={header} | |||
className="modal" | |||
overlayClassName="modal-overlay" | |||
onRequestClose={this.props.onClose}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
{this.state.loading && ( | |||
<div className="modal-body"> | |||
<i className="spinner" /> | |||
</div> | |||
)} | |||
{setting && ( | |||
<SettingForm | |||
branch={this.props.branch} | |||
onChange={this.props.onClose} | |||
onClose={this.props.onClose} | |||
project={this.props.project} | |||
setting={setting} | |||
/> | |||
)} | |||
</Modal> | |||
); | |||
} | |||
} |
@@ -0,0 +1,108 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 LongBranchesPatternForm from './LongBranchesPatternForm'; | |||
import { getValues, SettingValue } from '../../../api/settings'; | |||
import ChangeIcon from '../../../components/icons-components/ChangeIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
project: string; | |||
} | |||
interface State { | |||
formOpen: boolean; | |||
setting?: SettingValue; | |||
} | |||
export const LONG_BRANCH_PATTERN = 'sonar.branch.longLivedBranches.regex'; | |||
export default class LongBranchesPattern extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { formOpen: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchSetting(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchSetting() { | |||
return getValues(LONG_BRANCH_PATTERN, this.props.project).then( | |||
settings => { | |||
if (this.mounted) { | |||
this.setState({ setting: settings[0] }); | |||
} | |||
}, | |||
() => {} | |||
); | |||
} | |||
closeForm = () => { | |||
if (this.mounted) { | |||
this.setState({ formOpen: false }); | |||
} | |||
}; | |||
handleChangeClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.setState({ formOpen: true }); | |||
}; | |||
handleChange = () => { | |||
if (this.mounted) { | |||
this.fetchSetting().then(this.closeForm, this.closeForm); | |||
} | |||
}; | |||
render() { | |||
const { setting } = this.state; | |||
if (!setting) { | |||
return null; | |||
} | |||
return ( | |||
<div className="pull-right text-right"> | |||
{translate('branches.long_living_branches_pattern')} | |||
{': '} | |||
<strong>{setting.value}</strong> | |||
<a | |||
className="display-inline-block spacer-left link-no-underline" | |||
href="#" | |||
onClick={this.handleChangeClick}> | |||
<ChangeIcon /> | |||
</a> | |||
{this.state.formOpen && ( | |||
<LongBranchesPatternForm | |||
onClose={this.closeForm} | |||
onChange={this.handleChange} | |||
project={this.props.project} | |||
setting={setting} | |||
/> | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,49 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 Modal from 'react-modal'; | |||
import SettingForm from './SettingForm'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { SettingValue } from '../../../api/settings'; | |||
interface Props { | |||
onChange: () => void; | |||
onClose: () => void; | |||
project: string; | |||
setting: SettingValue; | |||
} | |||
export default function LongBranchesPatternForm(props: Props) { | |||
const header = translate('branches.detection_of_long_living_branches'); | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
contentLabel={header} | |||
className="modal" | |||
overlayClassName="modal-overlay" | |||
onRequestClose={props.onClose}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<SettingForm {...props} /> | |||
</Modal> | |||
); | |||
} |
@@ -0,0 +1,150 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 { SettingValue, setSimpleSettingValue, resetSettingValue } from '../../../api/settings'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
interface Props { | |||
branch?: string; | |||
onClose: () => void; | |||
onChange: () => void; | |||
project: string; | |||
setting: SettingValue; | |||
} | |||
interface State { | |||
submitting: boolean; | |||
value?: string; | |||
} | |||
export default class SettingForm extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { submitting: false, value: props.setting.value }; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const { value } = this.state; | |||
if (!value) { | |||
return; | |||
} | |||
this.setState({ submitting: true }); | |||
setSimpleSettingValue({ | |||
branch: this.props.branch, | |||
component: this.props.project, | |||
key: this.props.setting.key, | |||
value | |||
}).then(this.props.onChange, () => { | |||
if (this.mounted) { | |||
this.setState({ submitting: false }); | |||
} | |||
}); | |||
}; | |||
handleValueChange = (event: React.SyntheticEvent<HTMLInputElement>) => { | |||
this.setState({ value: event.currentTarget.value }); | |||
}; | |||
handleResetClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.setState({ submitting: true }); | |||
resetSettingValue(this.props.setting.key, this.props.project, this.props.branch).then( | |||
this.props.onChange, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ submitting: false }); | |||
} | |||
} | |||
); | |||
}; | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
}; | |||
render() { | |||
const { setting } = this.props; | |||
const submitDisabled = this.state.submitting || this.state.value === setting.value; | |||
return ( | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-body"> | |||
<div | |||
className="big-spacer-bottom markdown" | |||
dangerouslySetInnerHTML={{ __html: translate(`property.${setting.key}.description`) }} | |||
/> | |||
<div className="big-spacer-bottom"> | |||
<input | |||
autoFocus={true} | |||
className="input-super-large" | |||
onChange={this.handleValueChange} | |||
required={true} | |||
type="text" | |||
value={this.state.value} | |||
/> | |||
{setting.inherited && ( | |||
<div className="note spacer-top">{translate('settings._default')}</div> | |||
)} | |||
{!setting.inherited && | |||
setting.parentValue && ( | |||
<div className="note spacer-top"> | |||
{translateWithParameters('settings.default_x', setting.parentValue)} | |||
</div> | |||
)} | |||
</div> | |||
</div> | |||
<footer className="modal-foot"> | |||
{!setting.inherited && | |||
setting.parentValue && ( | |||
<button | |||
className="pull-left" | |||
disabled={this.state.submitting} | |||
onClick={this.handleResetClick} | |||
type="reset"> | |||
{translate('reset_to_default')} | |||
</button> | |||
)} | |||
{this.state.submitting && <i className="spinner spacer-right" />} | |||
<button disabled={submitDisabled} type="submit"> | |||
{translate('save')} | |||
</button> | |||
<a href="#" onClick={this.handleCancelClick}> | |||
{translate('cancel')} | |||
</a> | |||
</footer> | |||
</form> | |||
); | |||
} | |||
} |
@@ -17,18 +17,35 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
jest.mock('../../../../api/settings', () => ({ | |||
getValues: jest.fn(() => Promise.resolve([])) | |||
})); | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { mount, shallow } from 'enzyme'; | |||
import App from '../App'; | |||
import { Branch, BranchType } from '../../../../app/types'; | |||
const getValues = require('../../../../api/settings').getValues as jest.Mock<any>; | |||
beforeEach(() => { | |||
getValues.mockClear(); | |||
}); | |||
it('renders sorted list of branches', () => { | |||
const branches: Branch[] = [ | |||
{ isMain: true, name: 'master' }, | |||
{ isMain: false, name: 'branch-1.0', type: BranchType.LONG }, | |||
{ isMain: false, name: 'branch-1.0', mergeBranch: 'master', type: BranchType.SHORT } | |||
]; | |||
expect( | |||
shallow(<App branches={branches} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />) | |||
).toMatchSnapshot(); | |||
const wrapper = shallow( | |||
<App branches={branches} component={{ key: 'foo' }} onBranchesChange={jest.fn()} /> | |||
); | |||
wrapper.setState({ branchLifeTime: '100', loading: false }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('fetches branch life time setting on mount', () => { | |||
mount(<App branches={[]} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />); | |||
expect(getValues).toBeCalledWith('sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches'); | |||
}); |
@@ -26,6 +26,7 @@ import { click } from '../../../../helpers/testUtils'; | |||
const mainBranch: MainBranch = { isMain: true, name: 'master' }; | |||
const shortBranch: ShortLivingBranch = { | |||
analysisDate: '2017-09-27T00:05:19+0000', | |||
isMain: false, | |||
name: 'feature', | |||
mergeBranch: 'foo', |
@@ -0,0 +1,64 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
jest.mock('../../../../api/settings', () => ({ | |||
getValues: jest.fn(() => Promise.resolve([])) | |||
})); | |||
import * as React from 'react'; | |||
import { mount, shallow } from 'enzyme'; | |||
import LongBranchesPattern from '../LongBranchesPattern'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const getValues = require('../../../../api/settings').getValues as jest.Mock<any>; | |||
beforeEach(() => { | |||
getValues.mockClear(); | |||
}); | |||
it('renders', () => { | |||
const wrapper = shallow(<LongBranchesPattern project="project" />); | |||
wrapper.setState({ loading: false, setting: { value: 'release-.*' } }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('opens form', () => { | |||
const wrapper = shallow(<LongBranchesPattern project="project" />); | |||
(wrapper.instance() as LongBranchesPattern) .mounted = true; | |||
wrapper.setState({ loading: false, setting: { value: 'release-.*' } }); | |||
click(wrapper.find('a')); | |||
expect(wrapper.find('LongBranchesPatternForm').exists()).toBeTruthy(); | |||
wrapper.find('LongBranchesPatternForm').prop<Function>('onClose')(); | |||
expect(wrapper.find('LongBranchesPatternForm').exists()).toBeFalsy(); | |||
}); | |||
it('fetches setting value on mount', () => { | |||
mount(<LongBranchesPattern project="project" />); | |||
expect(getValues).lastCalledWith('sonar.branch.longLivedBranches.regex', 'project'); | |||
}); | |||
it('fetches new setting value after change', () => { | |||
const wrapper = mount(<LongBranchesPattern project="project" />); | |||
expect(getValues.mock.calls).toHaveLength(1); | |||
(wrapper.instance() as LongBranchesPattern).handleChange(); | |||
expect(getValues.mock.calls).toHaveLength(2); | |||
}); |
@@ -0,0 +1,35 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 { shallow } from 'enzyme'; | |||
import LongBranchesPatternForm from '../LongBranchesPatternForm'; | |||
it('renders', () => { | |||
expect( | |||
shallow( | |||
<LongBranchesPatternForm | |||
onChange={jest.fn()} | |||
onClose={jest.fn()} | |||
project="project" | |||
setting={{ key: 'foo', value: 'bar' }} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,82 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
jest.mock('../../../../api/settings', () => ({ | |||
setSimpleSettingValue: jest.fn(() => Promise.resolve()), | |||
resetSettingValue: jest.fn(() => Promise.resolve()) | |||
})); | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import SettingForm from '../SettingForm'; | |||
import { change, submit, click } from '../../../../helpers/testUtils'; | |||
const setSimpleSettingValue = require('../../../../api/settings') | |||
.setSimpleSettingValue as jest.Mock<any>; | |||
const resetSettingValue = require('../../../../api/settings').resetSettingValue as jest.Mock<any>; | |||
beforeEach(() => { | |||
setSimpleSettingValue.mockClear(); | |||
resetSettingValue.mockClear(); | |||
}); | |||
it('changes value', async () => { | |||
const onChange = jest.fn(); | |||
const wrapper = shallow( | |||
<SettingForm | |||
onChange={onChange} | |||
onClose={jest.fn()} | |||
project="project" | |||
setting={{ inherited: true, key: 'foo', value: 'release-.*' }} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
change(wrapper.find('input'), 'branch-.*'); | |||
submit(wrapper.find('form')); | |||
expect(setSimpleSettingValue).toBeCalledWith({ | |||
branch: undefined, | |||
component: 'project', | |||
key: 'foo', | |||
value: 'branch-.*' | |||
}); | |||
await new Promise(setImmediate); | |||
expect(onChange).toBeCalled(); | |||
}); | |||
it('resets value', async () => { | |||
const onChange = jest.fn(); | |||
const wrapper = shallow( | |||
<SettingForm | |||
onChange={onChange} | |||
onClose={jest.fn()} | |||
project="project" | |||
setting={{ inherited: false, key: 'foo', parentValue: 'branch-.*', value: 'release-.*' }} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('button[type="reset"]')); | |||
expect(resetSettingValue).toBeCalledWith('foo', 'project', undefined); | |||
await new Promise(setImmediate); | |||
expect(onChange).toBeCalled(); | |||
}); |
@@ -12,6 +12,34 @@ exports[`renders sorted list of branches 1`] = ` | |||
> | |||
project_branches.page | |||
</h1> | |||
<LongBranchesPattern | |||
project="foo" | |||
/> | |||
<p | |||
className="page-description" | |||
> | |||
project_branches.page.description | |||
</p> | |||
<p | |||
className="page-description" | |||
> | |||
<FormattedMessage | |||
defaultMessage="project_branches.page.life_time" | |||
id="project_branches.page.life_time" | |||
values={ | |||
Object { | |||
"days": "100", | |||
"settings": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/admin/settings" | |||
> | |||
settings.page | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</p> | |||
</header> | |||
<table | |||
className="data zebra zebra-hover" | |||
@@ -22,12 +50,17 @@ exports[`renders sorted list of branches 1`] = ` | |||
branch | |||
</th> | |||
<th | |||
className="text-right" | |||
className="thin nowrap text-right" | |||
> | |||
status | |||
</th> | |||
<th | |||
className="text-right" | |||
className="thin nowrap text-right" | |||
> | |||
project_history.last_snapshot | |||
</th> | |||
<th | |||
className="thin nowrap text-right" | |||
> | |||
actions | |||
</th> |
@@ -31,21 +31,45 @@ exports[`renders main branch 1`] = ` | |||
} | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-right" | |||
/> | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
<Tooltip | |||
overlay="branches.rename" | |||
placement="bottom" | |||
<div | |||
className="dropdown big-spacer-left" | |||
> | |||
<a | |||
className="js-rename link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
<button | |||
className="dropdown-toggle little-spacer-right button-compact" | |||
data-toggle="dropdown" | |||
> | |||
<SettingsIcon | |||
style={ | |||
Object { | |||
"marginTop": 4, | |||
} | |||
} | |||
/> | |||
<i | |||
className="icon-dropdown" | |||
/> | |||
</button> | |||
<ul | |||
className="dropdown-menu dropdown-menu-right" | |||
> | |||
<ChangeIcon /> | |||
</a> | |||
</Tooltip> | |||
<li> | |||
<a | |||
className="js-rename link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
branches.rename | |||
</a> | |||
</li> | |||
</ul> | |||
</div> | |||
</td> | |||
</tr> | |||
`; | |||
@@ -56,6 +80,7 @@ exports[`renders short-living branch 1`] = ` | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"analysisDate": "2017-09-27T00:05:19+0000", | |||
"isMain": false, | |||
"mergeBranch": "foo", | |||
"name": "feature", | |||
@@ -72,6 +97,7 @@ exports[`renders short-living branch 1`] = ` | |||
<BranchStatus | |||
branch={ | |||
Object { | |||
"analysisDate": "2017-09-27T00:05:19+0000", | |||
"isMain": false, | |||
"mergeBranch": "foo", | |||
"name": "feature", | |||
@@ -83,18 +109,46 @@ exports[`renders short-living branch 1`] = ` | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
<Tooltip | |||
overlay="branches.delete" | |||
placement="bottom" | |||
<DateFromNow | |||
date="2017-09-27T00:05:19+0000" | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
<div | |||
className="dropdown big-spacer-left" | |||
> | |||
<a | |||
className="js-delete link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
<button | |||
className="dropdown-toggle little-spacer-right button-compact" | |||
data-toggle="dropdown" | |||
> | |||
<SettingsIcon | |||
style={ | |||
Object { | |||
"marginTop": 4, | |||
} | |||
} | |||
/> | |||
<i | |||
className="icon-dropdown" | |||
/> | |||
</button> | |||
<ul | |||
className="dropdown-menu dropdown-menu-right" | |||
> | |||
<DeleteIcon /> | |||
</a> | |||
</Tooltip> | |||
<li> | |||
<a | |||
className="js-delete link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
branches.delete | |||
</a> | |||
</li> | |||
</ul> | |||
</div> | |||
</td> | |||
</tr> | |||
`; |
@@ -0,0 +1,20 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<div | |||
className="pull-right text-right" | |||
> | |||
branches.long_living_branches_pattern | |||
: | |||
<strong> | |||
release-.* | |||
</strong> | |||
<a | |||
className="display-inline-block spacer-left link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<ChangeIcon /> | |||
</a> | |||
</div> | |||
`; |
@@ -0,0 +1,36 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="branches.detection_of_long_living_branches" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
branches.detection_of_long_living_branches | |||
</h2> | |||
</header> | |||
<SettingForm | |||
onChange={[Function]} | |||
onClose={[Function]} | |||
project="project" | |||
setting={ | |||
Object { | |||
"key": "foo", | |||
"value": "bar", | |||
} | |||
} | |||
/> | |||
</Modal> | |||
`; |
@@ -0,0 +1,113 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`changes value 1`] = ` | |||
<form | |||
onSubmit={[Function]} | |||
> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="big-spacer-bottom markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "property.foo.description", | |||
} | |||
} | |||
/> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<input | |||
autoFocus={true} | |||
className="input-super-large" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="release-.*" | |||
/> | |||
<div | |||
className="note spacer-top" | |||
> | |||
settings._default | |||
</div> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={true} | |||
type="submit" | |||
> | |||
save | |||
</button> | |||
<a | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</form> | |||
`; | |||
exports[`resets value 1`] = ` | |||
<form | |||
onSubmit={[Function]} | |||
> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="big-spacer-bottom markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "property.foo.description", | |||
} | |||
} | |||
/> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<input | |||
autoFocus={true} | |||
className="input-super-large" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="release-.*" | |||
/> | |||
<div | |||
className="note spacer-top" | |||
> | |||
settings.default_x.branch-.* | |||
</div> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
className="pull-left" | |||
disabled={false} | |||
onClick={[Function]} | |||
type="reset" | |||
> | |||
reset_to_default | |||
</button> | |||
<button | |||
disabled={true} | |||
type="submit" | |||
> | |||
save | |||
</button> | |||
<a | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</form> | |||
`; |
@@ -22,7 +22,9 @@ import { RouterState, IndexRouteProps } from 'react-router'; | |||
const routes = [ | |||
{ | |||
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { | |||
import('./components/App').then(i => callback(null, { component: (i as any).default })); | |||
import('./components/AppContainer').then(i => | |||
callback(null, { component: (i as any).default }) | |||
); | |||
} | |||
} | |||
]; |
@@ -20,22 +20,18 @@ | |||
// @flow | |||
import React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { Link } from 'react-router'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import PageHeader from './PageHeader'; | |||
import CategoryDefinitionsList from './CategoryDefinitionsList'; | |||
import AllCategoriesList from './AllCategoriesList'; | |||
import WildcardsHelp from './WildcardsHelp'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import '../styles.css'; | |||
/*:: | |||
type Props = { | |||
branch?: {}, | |||
component?: { key: string }, | |||
defaultCategory: ?string, | |||
fetchSettings(componentKey: ?string, branch?: string): Promise<*>, | |||
fetchSettings(componentKey: ?string): Promise<*>, | |||
location: { query: {} } | |||
}; | |||
*/ | |||
@@ -56,15 +52,13 @@ export default class App extends React.PureComponent { | |||
html.classList.add('dashboard-page'); | |||
} | |||
const componentKey = this.props.component ? this.props.component.key : null; | |||
const branch = this.props.branch && getBranchName(this.props.branch); | |||
this.props.fetchSettings(componentKey, branch).then(() => this.setState({ loaded: true })); | |||
this.props.fetchSettings(componentKey).then(() => this.setState({ loaded: true })); | |||
} | |||
componentDidUpdate(prevProps /*: Props*/) { | |||
if (prevProps.component !== this.props.component) { | |||
const componentKey = this.props.component ? this.props.component.key : null; | |||
const branch = this.props.branch && getBranchName(this.props.branch); | |||
this.props.fetchSettings(componentKey, branch); | |||
this.props.fetchSettings(componentKey); | |||
} | |||
} | |||
@@ -83,51 +77,22 @@ export default class App extends React.PureComponent { | |||
const { query } = this.props.location; | |||
const selectedCategory = query.category || this.props.defaultCategory; | |||
const branchName = this.props.branch && getBranchName(this.props.branch); | |||
return ( | |||
<div id="settings-page" className="page page-limited"> | |||
<Helmet title={translate('settings.page')} /> | |||
{branchName ? ( | |||
<div className="alert alert-info"> | |||
<FormattedMessage | |||
defaultMessage={translate('branches.settings_hint')} | |||
id="branches.settings_hint" | |||
values={{ | |||
link: ( | |||
<Link | |||
to={{ | |||
pathname: '/project/branches', | |||
query: { id: this.props.component && this.props.component.key } | |||
}}> | |||
{translate('branches.settings_hint_tab')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
</div> | |||
) : ( | |||
<PageHeader branch={branchName} component={this.props.component} /> | |||
)} | |||
<PageHeader component={this.props.component} /> | |||
<div className="side-tabs-layout settings-layout"> | |||
{branchName == null && ( | |||
<div className="side-tabs-side"> | |||
<AllCategoriesList | |||
branch={branchName} | |||
component={this.props.component} | |||
selectedCategory={selectedCategory} | |||
defaultCategory={this.props.defaultCategory} | |||
/> | |||
</div> | |||
)} | |||
<div className="side-tabs-main"> | |||
<CategoryDefinitionsList | |||
branch={branchName} | |||
<div className="side-tabs-side"> | |||
<AllCategoriesList | |||
component={this.props.component} | |||
category={selectedCategory} | |||
selectedCategory={selectedCategory} | |||
defaultCategory={this.props.defaultCategory} | |||
/> | |||
</div> | |||
<div className="side-tabs-main"> | |||
<CategoryDefinitionsList component={this.props.component} category={selectedCategory} /> | |||
{selectedCategory === 'exclusions' && <WildcardsHelp />} | |||
</div> | |||
</div> |
@@ -32,7 +32,6 @@ type Category = { | |||
/*:: | |||
type Props = { | |||
branch?: string, | |||
categories: Category[], | |||
component?: { key: string }, | |||
defaultCategory: string, | |||
@@ -44,7 +43,7 @@ export default class CategoriesList extends React.PureComponent { | |||
/*:: rops: Props; */ | |||
renderLink(category /*: Category */) { | |||
const query /*: Object */ = { branch: this.props.branch }; | |||
const query /*: Object */ = {}; | |||
if (category.key !== this.props.defaultCategory) { | |||
query.category = category.key.toLowerCase(); |
@@ -47,7 +47,6 @@ class Definition extends React.PureComponent { | |||
/*:: timeout: number; */ | |||
static propTypes = { | |||
branch: PropTypes.string, | |||
component: PropTypes.object, | |||
setting: PropTypes.object.isRequired, | |||
changedValue: PropTypes.any, | |||
@@ -91,7 +90,7 @@ class Definition extends React.PureComponent { | |||
const componentKey = this.props.component ? this.props.component.key : null; | |||
const { definition } = this.props.setting; | |||
return this.props | |||
.resetValue(definition.key, componentKey, this.props.branch) | |||
.resetValue(definition.key, componentKey) | |||
.then(() => { | |||
this.safeSetState({ success: true }); | |||
this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); | |||
@@ -111,7 +110,7 @@ class Definition extends React.PureComponent { | |||
this.safeSetState({ success: false }); | |||
const componentKey = this.props.component ? this.props.component.key : null; | |||
this.props | |||
.saveValue(this.props.setting.definition.key, componentKey, this.props.branch) | |||
.saveValue(this.props.setting.definition.key, componentKey) | |||
.then(() => { | |||
this.safeSetState({ success: true }); | |||
this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); |
@@ -24,7 +24,6 @@ import Definition from './Definition'; | |||
export default class DefinitionsList extends React.PureComponent { | |||
static propTypes = { | |||
branch: PropTypes.string, | |||
component: PropTypes.object, | |||
settings: PropTypes.array.isRequired | |||
}; | |||
@@ -34,11 +33,7 @@ export default class DefinitionsList extends React.PureComponent { | |||
<ul className="settings-definitions-list"> | |||
{this.props.settings.map(setting => ( | |||
<li key={setting.definition.key}> | |||
<Definition | |||
branch={this.props.branch} | |||
component={this.props.component} | |||
setting={setting} | |||
/> | |||
<Definition component={this.props.component} setting={setting} /> | |||
</li> | |||
))} | |||
</ul> |
@@ -24,7 +24,6 @@ import { translate } from '../../../helpers/l10n'; | |||
export default class PageHeader extends React.PureComponent { | |||
static propTypes = { | |||
branch: PropTypes.string, | |||
component: PropTypes.object | |||
}; | |||
@@ -27,7 +27,6 @@ import { getSubCategoryName, getSubCategoryDescription } from '../utils'; | |||
export default class SubCategoryDefinitionsList extends React.PureComponent { | |||
static propTypes = { | |||
branch: PropTypes.string, | |||
component: PropTypes.object, | |||
settings: PropTypes.array.isRequired | |||
}; | |||
@@ -63,7 +62,6 @@ export default class SubCategoryDefinitionsList extends React.PureComponent { | |||
/> | |||
)} | |||
<DefinitionsList | |||
branch={this.props.branch} | |||
component={this.props.component} | |||
settings={bySubCategory[subCategory.key]} | |||
/> |
@@ -34,13 +34,19 @@ import { isEmptyValue } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getSettingsAppDefinition, getSettingsAppChangedValue } from '../../../store/rootReducer'; | |||
export const fetchSettings = (componentKey, branch) => dispatch => { | |||
return getDefinitions(componentKey, branch) | |||
export const fetchSettings = componentKey => dispatch => { | |||
return getDefinitions(componentKey) | |||
.then(definitions => { | |||
const withoutLicenses = definitions.filter(definition => definition.type !== 'LICENSE'); | |||
dispatch(receiveDefinitions(withoutLicenses)); | |||
const keys = withoutLicenses.map(definition => definition.key).join(); | |||
return getValues(keys, componentKey, branch); | |||
const filtered = definitions | |||
.filter(definition => definition.type !== 'LICENSE') | |||
// do not display this setting on project level | |||
.filter( | |||
definition => | |||
componentKey == null || definition.key !== 'sonar.branch.longLivedBranches.regex' | |||
); | |||
dispatch(receiveDefinitions(filtered)); | |||
const keys = filtered.map(definition => definition.key).join(); | |||
return getValues(keys, componentKey); | |||
}) | |||
.then(settings => { | |||
dispatch(receiveValues(settings, componentKey)); | |||
@@ -49,7 +55,7 @@ export const fetchSettings = (componentKey, branch) => dispatch => { | |||
.catch(e => parseError(e).then(message => dispatch(addGlobalErrorMessage(message)))); | |||
}; | |||
export const saveValue = (key, componentKey, branch) => (dispatch, getState) => { | |||
export const saveValue = (key, componentKey) => (dispatch, getState) => { | |||
dispatch(startLoading(key)); | |||
const state = getState(); | |||
@@ -62,8 +68,8 @@ export const saveValue = (key, componentKey, branch) => (dispatch, getState) => | |||
return Promise.reject(); | |||
} | |||
return setSettingValue(definition, value, componentKey, branch) | |||
.then(() => getValues(key, componentKey, branch)) | |||
return setSettingValue(definition, value, componentKey) | |||
.then(() => getValues(key, componentKey)) | |||
.then(values => { | |||
dispatch(receiveValues(values, componentKey)); | |||
dispatch(cancelChange(key)); | |||
@@ -77,11 +83,11 @@ export const saveValue = (key, componentKey, branch) => (dispatch, getState) => | |||
}); | |||
}; | |||
export const resetValue = (key, componentKey, branch) => dispatch => { | |||
export const resetValue = (key, componentKey) => dispatch => { | |||
dispatch(startLoading(key)); | |||
return resetSettingValue(key, componentKey, branch) | |||
.then(() => getValues(key, componentKey, branch)) | |||
return resetSettingValue(key, componentKey) | |||
.then(() => getValues(key, componentKey)) | |||
.then(values => { | |||
if (values.length > 0) { | |||
dispatch(receiveValues(values, componentKey)); |
@@ -0,0 +1,51 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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'; | |||
interface Props { | |||
className?: string; | |||
fill?: string; | |||
size?: number; | |||
style?: React.CSSProperties; | |||
} | |||
export default function SettingsIcon({ | |||
className, | |||
fill = 'currentColor', | |||
size = 14, | |||
style | |||
}: Props) { | |||
return ( | |||
<svg | |||
className={className} | |||
xmlns="http://www.w3.org/2000/svg" | |||
viewBox="0 0 14 14" | |||
width={size} | |||
height={size} | |||
style={style}> | |||
<g transform="matrix(0.0364583,0,0,0.0364583,0,-1.16667)"> | |||
<path | |||
d="M256,224C256,206.333 249.75,191.25 237.25,178.75C224.75,166.25 209.667,160 192,160C174.333,160 159.25,166.25 146.75,178.75C134.25,191.25 128,206.333 128,224C128,241.667 134.25,256.75 146.75,269.25C159.25,281.75 174.333,288 192,288C209.667,288 224.75,281.75 237.25,269.25C249.75,256.75 256,241.667 256,224ZM384,196.75L384,252.25C384,254.25 383.333,256.167 382,258C380.667,259.833 379,260.917 377,261.25L330.75,268.25C327.583,277.25 324.333,284.833 321,291C326.833,299.333 335.75,310.833 347.75,325.5C349.417,327.5 350.25,329.583 350.25,331.75C350.25,333.917 349.5,335.833 348,337.5C343.5,343.667 335.25,352.667 323.25,364.5C311.25,376.333 303.417,382.25 299.75,382.25C297.75,382.25 295.583,381.5 293.25,380L258.75,353C251.417,356.833 243.833,360 236,362.5C233.333,385.167 230.917,400.667 228.75,409C227.583,413.667 224.583,416 219.75,416L164.25,416C161.917,416 159.875,415.292 158.125,413.875C156.375,412.458 155.417,410.667 155.25,408.5L148.25,362.5C140.083,359.833 132.583,356.75 125.75,353.25L90.5,380C88.833,381.5 86.75,382.25 84.25,382.25C81.917,382.25 79.833,381.333 78,379.5C57,360.5 43.25,346.5 36.75,337.5C35.583,335.833 35,333.917 35,331.75C35,329.75 35.667,327.833 37,326C39.5,322.5 43.75,316.958 49.75,309.375C55.75,301.792 60.25,295.917 63.25,291.75C58.75,283.417 55.333,275.167 53,267L7.25,260.25C5.083,259.917 3.333,258.875 2,257.125C0.667,255.375 0,253.417 0,251.25L0,195.75C0,193.75 0.667,191.833 2,190C3.333,188.167 4.917,187.083 6.75,186.75L53.25,179.75C55.583,172.083 58.833,164.417 63,156.75C56.333,147.25 47.417,135.75 36.25,122.25C34.583,120.25 33.75,118.25 33.75,116.25C33.75,114.583 34.5,112.667 36,110.5C40.333,104.5 48.542,95.542 60.625,83.625C72.708,71.708 80.583,65.75 84.25,65.75C86.417,65.75 88.583,66.583 90.75,68.25L125.25,95C132.583,91.167 140.167,88 148,85.5C150.667,62.833 153.083,47.333 155.25,39C156.417,34.333 159.417,32 164.25,32L219.75,32C222.083,32 224.125,32.708 225.875,34.125C227.625,35.542 228.583,37.333 228.75,39.5L235.75,85.5C243.917,88.167 251.417,91.25 258.25,94.75L293.75,68C295.25,66.5 297.25,65.75 299.75,65.75C301.917,65.75 304,66.583 306,68.25C327.5,88.083 341.25,102.25 347.25,110.75C348.417,112.083 349,113.917 349,116.25C349,118.25 348.333,120.167 347,122C344.5,125.5 340.25,131.042 334.25,138.625C328.25,146.208 323.75,152.083 320.75,156.25C325.083,164.583 328.5,172.75 331,180.75L376.75,187.75C378.917,188.083 380.667,189.125 382,190.875C383.333,192.625 384,194.583 384,196.75Z" | |||
style={{ fill }} | |||
/> | |||
</g> | |||
</svg> | |||
); | |||
} |
@@ -228,11 +228,11 @@ ul.modal-head-metadata li { | |||
} | |||
.modal-foot { | |||
text-align: right; | |||
padding: 8px 10px; | |||
line-height: 24px; | |||
padding: 10px; | |||
border-top: 1px solid #ccc; | |||
line-height: 30px; | |||
background-color: #efefef; | |||
text-align: right; | |||
button, | |||
.button, |
@@ -343,9 +343,6 @@ a:hover > .icon-radio { | |||
content: '\f03a'; | |||
font-size: @iconSmallFontSize; | |||
} | |||
.icon-settings:before { | |||
content: '\f015'; | |||
} | |||
.icon-bulk-change:before { | |||
content: '\f085'; | |||
font-size: @iconSmallFontSize; |
@@ -143,6 +143,7 @@ reload=Reload | |||
remove=Remove | |||
rename=Rename | |||
reset_verb=Reset | |||
reset_to_default=Reset To Default | |||
resolution=Resolution | |||
restart=Restart | |||
restore=Restore | |||
@@ -591,6 +592,9 @@ application_deletion.page.description=Delete this application. Application proje | |||
provisioning.page=Provisioning | |||
provisioning.page.description=Use this page to initialize projects if you would like to configure them before the first analysis. Once a project is provisioned, you have access to perform all project configurations on it. | |||
project_branches.page=Branches | |||
project_branches.page.description=Use this page to manage project branches. | |||
project_branches.page.life_time=Short living branches are permanently deleted when there is no analysis for {days} days. | |||
project_branches.page.life_time.admin=Short living branches are permanently deleted when there is no analysis for {days} days. You can change this parameter in {settings}. | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -1073,6 +1077,8 @@ property.error.notFloat=Not a floating point number | |||
property.error.notRegexp=Not a valid Java regular expression | |||
property.error.notInOptions=Not a valid option | |||
property.category.scm=SCM | |||
property.sonar.leak.period.description=Period used to compare measures and track new issues. Values are:<ul class='bullet'><li>Number of days before analysis, for example 5.</li><li>A custom date. Format is yyyy-MM-dd, for example 2010-12-25</li><li>'previous_version' to compare to the previous version in the project history</li><li>A version, for example '1.2' or 'BASELINE'</li></ul><p>When specifying a number of days or a date, the snapshot selected for comparison is the first one available inside the corresponding time range. </p><p>Changing this property only takes effect after subsequent project inspections.<p/> | |||
property.sonar.branch.longLivedBranches.regex.description=Regular expression used to detect whether a branch is a long living branch (as opposed to short living branch), based on its name. This applies only during first analysis, the type of a branch cannot be changed later. | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -3207,8 +3213,10 @@ branches.orphan_branches=Orphan Branches | |||
branches.orphan_branches.tooltip=When a target branch of a short-living branch was deleted, this short-living branch becomes orphan. | |||
branches.main_branch=Main Branch | |||
branches.branch_settings=Branch Settings | |||
branches.settings_hint=To administrate your branches, you have to go to your main branch's {link} tab. | |||
branches.settings_hint_tab=Administration > Branches | |||
branches.long_living_branches_pattern=Long living branches pattern | |||
branches.detection_of_long_living_branches=Detection of long living branches | |||
branches.detection_of_long_living_branches.description=Regular expression used to detect whether a branch is a long living branch (as opposed to short living branch), based on its name. This applies only during first analysis, the type of a branch cannot be changed later. | |||
branches.set_leak_period=Set leak period | |||
#------------------------------------------------------------------------------ |