Browse Source

SONAR-9756 update branch management page

tags/6.6-RC1
Stas Vilchik 6 years ago
parent
commit
1b75e33aa6
36 changed files with 1249 additions and 181 deletions
  1. 24
    1
      server/sonar-web/src/main/js/api/settings.ts
  2. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  3. 0
    19
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  4. 3
    0
      server/sonar-web/src/main/js/app/types.ts
  5. 2
    1
      server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
  6. 12
    4
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
  7. 2
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
  8. 115
    28
      server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
  9. 28
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/AppContainer.ts
  10. 67
    18
      server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
  11. 106
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx
  12. 108
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx
  13. 49
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPatternForm.tsx
  14. 150
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx
  15. 21
    4
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
  16. 1
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
  17. 64
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx
  18. 35
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPatternForm-test.tsx
  19. 82
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx
  20. 35
    2
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
  21. 74
    20
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
  22. 20
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPattern-test.tsx.snap
  23. 36
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPatternForm-test.tsx.snap
  24. 113
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/SettingForm-test.tsx.snap
  25. 3
    1
      server/sonar-web/src/main/js/apps/projectBranches/routes.ts
  26. 12
    47
      server/sonar-web/src/main/js/apps/settings/components/App.js
  27. 1
    2
      server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
  28. 2
    3
      server/sonar-web/src/main/js/apps/settings/components/Definition.js
  29. 1
    6
      server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js
  30. 0
    1
      server/sonar-web/src/main/js/apps/settings/components/PageHeader.js
  31. 0
    2
      server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js
  32. 18
    12
      server/sonar-web/src/main/js/apps/settings/store/actions.js
  33. 51
    0
      server/sonar-web/src/main/js/components/icons-components/SettingsIcon.tsx
  34. 3
    3
      server/sonar-web/src/main/less/components/modals.less
  35. 0
    3
      server/sonar-web/src/main/less/init/icons.less
  36. 10
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 24
- 1
server/sonar-web/src/main/js/api/settings.ts View File

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

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

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


+ 0
- 19
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap View File

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


+ 3
- 0
server/sonar-web/src/main/js/app/types.ts View File

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

+ 2
- 1
server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js View File

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

+ 12
- 4
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap View File

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

+ 2
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js View File

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

+ 115
- 28
server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx View File

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

+ 28
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/AppContainer.ts View File

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

+ 67
- 18
server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx View File

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

+ 106
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx View File

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

+ 108
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx View File

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

+ 49
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPatternForm.tsx View File

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

+ 150
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx View File

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

+ 21
- 4
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx View File

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

+ 1
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx View File

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

+ 64
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx View File

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

+ 35
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPatternForm-test.tsx View File

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

+ 82
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx View File

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

+ 35
- 2
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap View File

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

+ 74
- 20
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap View File

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

+ 20
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPattern-test.tsx.snap View File

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

+ 36
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPatternForm-test.tsx.snap View File

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

+ 113
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/SettingForm-test.tsx.snap View File

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

+ 3
- 1
server/sonar-web/src/main/js/apps/projectBranches/routes.ts View File

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

+ 12
- 47
server/sonar-web/src/main/js/apps/settings/components/App.js View File

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

+ 1
- 2
server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js View File

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

+ 2
- 3
server/sonar-web/src/main/js/apps/settings/components/Definition.js View File

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

+ 1
- 6
server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js View File

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

+ 0
- 1
server/sonar-web/src/main/js/apps/settings/components/PageHeader.js View File

@@ -24,7 +24,6 @@ import { translate } from '../../../helpers/l10n';

export default class PageHeader extends React.PureComponent {
static propTypes = {
branch: PropTypes.string,
component: PropTypes.object
};


+ 0
- 2
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js View File

@@ -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]}
/>

+ 18
- 12
server/sonar-web/src/main/js/apps/settings/store/actions.js View File

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

+ 51
- 0
server/sonar-web/src/main/js/components/icons-components/SettingsIcon.tsx View File

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

+ 3
- 3
server/sonar-web/src/main/less/components/modals.less View File

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

+ 0
- 3
server/sonar-web/src/main/less/init/icons.less View File

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

+ 10
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

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


#------------------------------------------------------------------------------

Loading…
Cancel
Save