aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-09-27 15:08:22 +0200
committerDaniel Schwarz <bartfastiel@users.noreply.github.com>2017-10-03 08:47:46 +0200
commit1b75e33aa632a384e8c6b4c266a0c65ccd9fa5de (patch)
tree1b6d1ae2a96b46125f3fd6e4f821299281e1cc07 /server
parent969144e95d4abdac3e7efd0646390cfaaff3b0d1 (diff)
downloadsonarqube-1b75e33aa632a384e8c6b4c266a0c65ccd9fa5de.tar.gz
sonarqube-1b75e33aa632a384e8c6b4c266a0c65ccd9fa5de.zip
SONAR-9756 update branch management page
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/settings.ts25
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap19
-rw-r--r--server/sonar-web/src/main/js/app/types.ts3
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js3
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap16
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx143
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/AppContainer.ts28
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx85
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx106
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx108
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPatternForm.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx150
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPatternForm-test.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx82
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap37
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap94
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPattern-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPatternForm-test.tsx.snap36
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/SettingForm-test.tsx.snap113
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/routes.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/App.js59
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/Definition.js5
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js7
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/PageHeader.js1
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/actions.js30
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/SettingsIcon.tsx51
-rw-r--r--server/sonar-web/src/main/less/components/modals.less6
-rw-r--r--server/sonar-web/src/main/less/init/icons.less3
35 files changed, 1239 insertions, 179 deletions
diff --git a/server/sonar-web/src/main/js/api/settings.ts b/server/sonar-web/src/main/js/api/settings.ts
index ecadfadc87f..dd257cea597 100644
--- a/server/sonar-web/src/main/js/api/settings.ts
+++ b/server/sonar-web/src/main/js/api/settings.ts
@@ -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 });
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
index 9cc10de252e..8e118add3ef 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
index 60d527b6a42..4c3676fd64f 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
@@ -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>
`;
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index cc8717235f3..8bf75ae6e04 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -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;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
index b56f0a4e917..832d3050075 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
index 64b08bff996..0bfde40983a 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.js.snap
@@ -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
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
index 8b0ee390d57..b9ed0422bb7 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
@@ -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 &&
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
index 02a8e20365f..6336d24397f 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
@@ -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>
+ );
+ }
}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/AppContainer.ts b/server/sonar-web/src/main/js/apps/projectBranches/components/AppContainer.ts
new file mode 100644
index 00000000000..9e89531408b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/AppContainer.ts
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
index e163481d759..3cbc25df9a4 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx
new file mode 100644
index 00000000000..26d0c82f668
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx
new file mode 100644
index 00000000000..da026682bfe
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPatternForm.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPatternForm.tsx
new file mode 100644
index 00000000000..c773b3c737f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPatternForm.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx
new file mode 100644
index 00000000000..41e8c9d439e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
index 4288105f79a..9ffafb34d72 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
@@ -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');
});
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
index 4edc3ce70d6..667d377733f 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
@@ -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',
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx
new file mode 100644
index 00000000000..ff142d2065c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx
@@ -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);
+});
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPatternForm-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPatternForm-test.tsx
new file mode 100644
index 00000000000..d70e0a4f1bb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPatternForm-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx
new file mode 100644
index 00000000000..f74831cd522
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
index 6f983e33df8..349913a66e3 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
index ea135765ece..febb99812b8 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
@@ -33,19 +33,43 @@ 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>
`;
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPattern-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPattern-test.tsx.snap
new file mode 100644
index 00000000000..312bdd3d079
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPattern-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPatternForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPatternForm-test.tsx.snap
new file mode 100644
index 00000000000..d1c5ef0c609
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/LongBranchesPatternForm-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/SettingForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/SettingForm-test.tsx.snap
new file mode 100644
index 00000000000..bec3f234357
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/SettingForm-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/routes.ts b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts
index 520805ebac5..8cc627aeca7 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/routes.ts
+++ b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts
@@ -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 })
+ );
}
}
];
diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js
index 866830fbb69..7df88bb9fb7 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/App.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/App.js
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
index 8cbda6596c7..da25601fd40 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
@@ -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();
diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.js b/server/sonar-web/src/main/js/apps/settings/components/Definition.js
index 91eec4f2433..fc63e9d8930 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/Definition.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.js
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js
index 7cb111b297a..a04d9aa7203 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js
index d70bb1e1b13..c68594d8bef 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js
@@ -24,7 +24,6 @@ import { translate } from '../../../helpers/l10n';
export default class PageHeader extends React.PureComponent {
static propTypes = {
- branch: PropTypes.string,
component: PropTypes.object
};
diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js
index e779f1105c9..4a08b134fd1 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js
@@ -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]}
/>
diff --git a/server/sonar-web/src/main/js/apps/settings/store/actions.js b/server/sonar-web/src/main/js/apps/settings/store/actions.js
index 25e75c2b3c3..8c42137cf57 100644
--- a/server/sonar-web/src/main/js/apps/settings/store/actions.js
+++ b/server/sonar-web/src/main/js/apps/settings/store/actions.js
@@ -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));
diff --git a/server/sonar-web/src/main/js/components/icons-components/SettingsIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/SettingsIcon.tsx
new file mode 100644
index 00000000000..3c8d8d5ac5b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/SettingsIcon.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/less/components/modals.less b/server/sonar-web/src/main/less/components/modals.less
index 3121933a32a..d4655515163 100644
--- a/server/sonar-web/src/main/less/components/modals.less
+++ b/server/sonar-web/src/main/less/components/modals.less
@@ -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,
diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less
index 2c7b0e75f88..fcdfb29ff25 100644
--- a/server/sonar-web/src/main/less/init/icons.less
+++ b/server/sonar-web/src/main/less/init/icons.less
@@ -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;