aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-docs/package.json2
-rw-r--r--server/sonar-docs/yarn.lock8
-rw-r--r--server/sonar-web/package.json2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx56
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx92
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx113
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx168
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx186
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx76
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx135
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx39
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/AppHeader-test.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx117
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisList-test.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisListRenderer-test.tsx84
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx103
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap47
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap80
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap210
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisListRenderer-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisList-test.tsx.snap)146
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap104
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchList-test.tsx.snap197
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchListRow-test.tsx.snap308
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap37
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts40
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/styles.css9
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/utils.ts18
-rw-r--r--server/sonar-web/src/main/js/types/types.d.ts3
-rw-r--r--server/sonar-web/yarn.lock8
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties30
36 files changed, 2087 insertions, 570 deletions
diff --git a/server/sonar-docs/package.json b/server/sonar-docs/package.json
index 439c21f8efc..f6d9fc44355 100644
--- a/server/sonar-docs/package.json
+++ b/server/sonar-docs/package.json
@@ -21,7 +21,7 @@
"react-dom": "16.13.0",
"react-helmet": "5.2.1",
"react-typography": "0.16.19",
- "sonar-ui-common": "1.0.4",
+ "sonar-ui-common": "1.0.6",
"typography": "0.16.19"
},
"devDependencies": {
diff --git a/server/sonar-docs/yarn.lock b/server/sonar-docs/yarn.lock
index 2c24a788a43..d76e4819e3d 100644
--- a/server/sonar-docs/yarn.lock
+++ b/server/sonar-docs/yarn.lock
@@ -12736,10 +12736,10 @@ sockjs@0.3.19:
faye-websocket "^0.10.0"
uuid "^3.0.1"
-sonar-ui-common@1.0.4:
- version "1.0.4"
- resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-1.0.4.tgz#342cb674a560a79cbae47ecbf60ab47fe1052fa4"
- integrity sha1-NCy2dKVgp5y65H7L9gq0f+EFL6Q=
+sonar-ui-common@1.0.6:
+ version "1.0.6"
+ resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-1.0.6.tgz#0b22b9b35e4e210b34304780328b9831bf1f7a58"
+ integrity sha1-CyK5s15OIQs0MEeAMouYMb8felg=
dependencies:
"@types/react-select" "1.2.6"
classnames "2.2.6"
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index ee9eca8c188..8ba8434cfa3 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -38,7 +38,7 @@
"rehype-slug": "3.0.0",
"remark-custom-blocks": "2.5.0",
"remark-rehype": "6.0.0",
- "sonar-ui-common": "1.0.4",
+ "sonar-ui-common": "1.0.6",
"unist-util-visit": "2.0.2",
"valid-url": "1.0.9",
"whatwg-fetch": "3.0.0"
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
index a2817bcd36f..45b0f93ce0f 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
@@ -70,41 +70,39 @@ export function BranchOverviewRenderer(props: BranchOverviewRendererProps) {
{projectIsEmpty ? (
<NoCodeWarning branchLike={branchLike} component={component} measures={measures} />
) : (
- <>
- <div className="display-flex-row">
- <div className="width-25 big-spacer-right">
- <QualityGatePanel
+ <div className="display-flex-row">
+ <div className="width-25 big-spacer-right">
+ <QualityGatePanel
+ component={component}
+ loading={loadingStatus}
+ qgStatuses={qgStatuses}
+ />
+ </div>
+
+ <div className="flex-1">
+ <div className="display-flex-column">
+ <MeasuresPanel
+ branchLike={branchLike}
component={component}
+ leakPeriod={leakPeriod}
loading={loadingStatus}
- qgStatuses={qgStatuses}
+ measures={measures}
/>
- </div>
- <div className="flex-1">
- <div className="display-flex-column">
- <MeasuresPanel
- branchLike={branchLike}
- component={component}
- leakPeriod={leakPeriod}
- loading={loadingStatus}
- measures={measures}
- />
-
- <ActivityPanel
- analyses={analyses}
- branchLike={branchLike}
- component={component}
- graph={graph}
- leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)}
- loading={loadingHistory}
- measuresHistory={measuresHistory}
- metrics={metrics}
- onGraphChange={onGraphChange}
- />
- </div>
+ <ActivityPanel
+ analyses={analyses}
+ branchLike={branchLike}
+ component={component}
+ graph={graph}
+ leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)}
+ loading={loadingHistory}
+ measuresHistory={measuresHistory}
+ metrics={metrics}
+ onGraphChange={onGraphChange}
+ />
</div>
</div>
- </>
+ </div>
)}
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
index 08c4b262f0a..f6ee76e4833 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
@@ -20,16 +20,16 @@
import * as classNames from 'classnames';
import { debounce } from 'lodash';
import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router';
import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import { BranchLike } from '../../../types/branch-like';
+import { isBranch, sortBranches } from '../../../helpers/branch-like';
+import { Branch, BranchLike } from '../../../types/branch-like';
import '../styles.css';
import { getSettingValue } from '../utils';
+import AppHeader from './AppHeader';
import BranchList from './BranchList';
import ProjectBaselineSelector from './ProjectBaselineSelector';
@@ -42,17 +42,21 @@ interface Props {
interface State {
analysis?: string;
+ branchList: Branch[];
currentSetting?: T.NewCodePeriodSettingType;
currentSettingValue?: string;
days: string;
generalSetting?: T.NewCodePeriod;
loading: boolean;
overrideGeneralSetting?: boolean;
+ referenceBranch?: string;
saving: boolean;
selected?: T.NewCodePeriodSettingType;
success?: boolean;
}
+const DEFAULT_NUMBER_OF_DAYS = '30';
+
const DEFAULT_GENERAL_SETTING: { type: T.NewCodePeriodSettingType } = {
type: 'PREVIOUS_VERSION'
};
@@ -60,7 +64,8 @@ const DEFAULT_GENERAL_SETTING: { type: T.NewCodePeriodSettingType } = {
export default class App extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
- days: '30',
+ branchList: [],
+ days: DEFAULT_NUMBER_OF_DAYS,
loading: true,
saving: false
};
@@ -71,6 +76,13 @@ export default class App extends React.PureComponent<Props, State> {
componentDidMount() {
this.mounted = true;
this.fetchLeakPeriodSetting();
+ this.sortAndFilterBranches(this.props.branchLikes);
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.branchLikes !== this.props.branchLikes) {
+ this.sortAndFilterBranches(this.props.branchLikes);
+ }
}
componentWillUnmount() {
@@ -83,9 +95,10 @@ export default class App extends React.PureComponent<Props, State> {
generalSetting: T.NewCodePeriod;
}) {
const { currentSetting, currentSettingValue, generalSetting } = params;
+ const { referenceBranch } = this.state;
const defaultDays =
- (!currentSetting && generalSetting.type === 'NUMBER_OF_DAYS' && generalSetting.value) || '30';
+ (generalSetting.type === 'NUMBER_OF_DAYS' && generalSetting.value) || DEFAULT_NUMBER_OF_DAYS;
return {
loading: false,
@@ -95,10 +108,17 @@ export default class App extends React.PureComponent<Props, State> {
selected: currentSetting || generalSetting.type,
overrideGeneralSetting: Boolean(currentSetting),
days: (currentSetting === 'NUMBER_OF_DAYS' && currentSettingValue) || defaultDays,
- analysis: (currentSetting === 'SPECIFIC_ANALYSIS' && currentSettingValue) || ''
+ analysis: (currentSetting === 'SPECIFIC_ANALYSIS' && currentSettingValue) || '',
+ referenceBranch:
+ (currentSetting === 'REFERENCE_BRANCH' && currentSettingValue) || referenceBranch
};
}
+ sortAndFilterBranches(branchLikes: BranchLike[] = []) {
+ const branchList = sortBranches(branchLikes.filter(isBranch));
+ this.setState({ branchList, referenceBranch: branchList[0].name });
+ }
+
fetchLeakPeriodSetting() {
this.setState({ loading: true });
@@ -118,7 +138,11 @@ export default class App extends React.PureComponent<Props, State> {
const currentSetting = setting.inherited ? undefined : setting.type || 'PREVIOUS_VERSION';
this.setState(
- this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue })
+ this.getUpdatedState({
+ generalSetting,
+ currentSetting,
+ currentSettingValue
+ })
);
}
},
@@ -150,6 +174,10 @@ export default class App extends React.PureComponent<Props, State> {
handleSelectDays = (days: string) => this.setState({ days });
+ handleSelectReferenceBranch = (referenceBranch: string) => {
+ this.setState({ referenceBranch });
+ };
+
handleCancel = () =>
this.setState(
({ generalSetting = DEFAULT_GENERAL_SETTING, currentSetting, currentSettingValue }) =>
@@ -165,14 +193,14 @@ export default class App extends React.PureComponent<Props, State> {
e.preventDefault();
const { component } = this.props;
- const { analysis, days, selected: type, overrideGeneralSetting } = this.state;
+ const { analysis, days, selected: type, referenceBranch, overrideGeneralSetting } = this.state;
if (!overrideGeneralSetting) {
this.resetSetting();
return;
}
- const value = getSettingValue({ type, analysis, days });
+ const value = getSettingValue({ type, analysis, days, referenceBranch });
if (type) {
this.setState({ saving: true });
@@ -197,51 +225,18 @@ export default class App extends React.PureComponent<Props, State> {
}
};
- renderHeader() {
- return (
- <header className="page-header">
- <h1 className="page-title">{translate('project_baseline.page')}</h1>
- <p className="page-description">
- <FormattedMessage
- defaultMessage={translate('project_baseline.page.description')}
- id="project_baseline.page.description"
- values={{
- link: (
- <Link to="/documentation/project-administration/new-code-period/">
- {translate('project_baseline.page.description.link')}
- </Link>
- )
- }}
- />
- <br />
- {this.props.canAdmin && (
- <FormattedMessage
- defaultMessage={translate('project_baseline.page.description2')}
- id="project_baseline.page.description2"
- values={{
- link: (
- <Link to="/admin/settings?category=new_code_period">
- {translate('project_baseline.page.description2.link')}
- </Link>
- )
- }}
- />
- )}
- </p>
- </header>
- );
- }
-
render() {
- const { branchLikes, branchesEnabled, component } = this.props;
+ const { branchesEnabled, canAdmin, component } = this.props;
const {
analysis,
+ branchList,
currentSetting,
days,
generalSetting,
loading,
currentSettingValue,
overrideGeneralSetting,
+ referenceBranch,
saving,
selected,
success
@@ -251,7 +246,7 @@ export default class App extends React.PureComponent<Props, State> {
<>
<Suggestions suggestions="project_baseline" />
<div className="page page-limited">
- {this.renderHeader()}
+ <AppHeader canAdmin={!!canAdmin} />
{loading ? (
<DeferredSpinner />
) : (
@@ -261,6 +256,7 @@ export default class App extends React.PureComponent<Props, State> {
{generalSetting && overrideGeneralSetting !== undefined && (
<ProjectBaselineSelector
analysis={analysis}
+ branchList={branchList}
branchesEnabled={branchesEnabled}
component={component.key}
currentSetting={currentSetting}
@@ -270,10 +266,12 @@ export default class App extends React.PureComponent<Props, State> {
onCancel={this.handleCancel}
onSelectAnalysis={this.handleSelectAnalysis}
onSelectDays={this.handleSelectDays}
+ onSelectReferenceBranch={this.handleSelectReferenceBranch}
onSelectSetting={this.handleSelectSetting}
onSubmit={this.handleSubmit}
onToggleSpecificSetting={this.handleToggleSpecificSetting}
overrideGeneralSetting={overrideGeneralSetting}
+ referenceBranch={referenceBranch}
saving={saving}
selected={selected}
/>
@@ -290,7 +288,7 @@ export default class App extends React.PureComponent<Props, State> {
<hr />
<h2>{translate('project_baseline.configure_branches')}</h2>
<BranchList
- branchLikes={branchLikes}
+ branchList={branchList}
component={component}
inheritedSetting={
currentSetting
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx
new file mode 100644
index 00000000000..102c39034f3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+
+export interface AppHeaderProps {
+ canAdmin: boolean;
+}
+
+export default function AppHeader(props: AppHeaderProps) {
+ const { canAdmin } = props;
+
+ return (
+ <header className="page-header">
+ <h1 className="page-title">{translate('project_baseline.page')}</h1>
+ <p className="page-description">
+ <FormattedMessage
+ defaultMessage={translate('project_baseline.page.description')}
+ id="project_baseline.page.description"
+ values={{
+ link: (
+ <Link to="/documentation/project-administration/new-code-period/">
+ {translate('project_baseline.page.description.link')}
+ </Link>
+ )
+ }}
+ />
+ <br />
+ {canAdmin && (
+ <FormattedMessage
+ defaultMessage={translate('project_baseline.page.description2')}
+ id="project_baseline.page.description2"
+ values={{
+ link: (
+ <Link to="/admin/settings?category=new_code_period">
+ {translate('project_baseline.page.description2.link')}
+ </Link>
+ )
+ }}
+ />
+ )}
+ </p>
+ </header>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
new file mode 100644
index 00000000000..d9b2f035fa6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import RadioCard from 'sonar-ui-common/components/controls/RadioCard';
+import SearchSelect from 'sonar-ui-common/components/controls/SearchSelect';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
+import AlertErrorIcon from 'sonar-ui-common/components/icons/AlertErrorIcon';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+
+export interface BaselineSettingReferenceBranchProps {
+ branchList: BranchOption[];
+ className?: string;
+ configuredBranchName?: string;
+ disabled?: boolean;
+ onChangeReferenceBranch: (value: string) => void;
+ onSelect: (selection: T.NewCodePeriodSettingType) => void;
+ referenceBranch: string;
+ selected: boolean;
+ settingLevel: 'project' | 'branch';
+}
+
+export interface BranchOption {
+ disabled?: boolean;
+ isInvalid?: boolean;
+ isMain: boolean;
+ value: string;
+}
+
+function renderBranchOption(option: BranchOption) {
+ return option.isInvalid ? (
+ <Tooltip
+ overlay={translateWithParameters('baseline.reference_branch.does_not_exist', option.value)}>
+ <span>
+ {option.value} <AlertErrorIcon />
+ </span>
+ </Tooltip>
+ ) : (
+ <>
+ <span
+ title={
+ option.disabled ? translate('baseline.reference_branch.cannot_be_itself') : undefined
+ }>
+ {option.value}
+ </span>
+ {option.isMain && (
+ <div className="badge spacer-left">{translate('branches.main_branch')}</div>
+ )}
+ </>
+ );
+}
+
+export default function BaselineSettingReferenceBranch(props: BaselineSettingReferenceBranchProps) {
+ const { branchList, className, disabled, referenceBranch, selected, settingLevel } = props;
+
+ const currentBranch = branchList.find(b => b.value === referenceBranch) || {
+ value: referenceBranch,
+ isMain: false,
+ isInvalid: true
+ };
+
+ return (
+ <RadioCard
+ className={className}
+ disabled={disabled}
+ onClick={() => props.onSelect('REFERENCE_BRANCH')}
+ selected={selected}
+ title={translate('baseline.reference_branch')}>
+ <>
+ <p>{translate('baseline.reference_branch.description')}</p>
+ {selected && (
+ <>
+ {settingLevel === 'project' && (
+ <p className="spacer-top">{translate('baseline.reference_branch.description2')}</p>
+ )}
+ <div className="big-spacer-top display-flex-column">
+ <label className="text-middle" htmlFor="reference_branch">
+ <strong>{translate('baseline.reference_branch.choose')}</strong>
+ <em className="mandatory">*</em>
+ </label>
+ <SearchSelect<BranchOption>
+ autofocus={false}
+ className="little-spacer-top spacer-bottom"
+ defaultOptions={branchList}
+ minimumQueryLength={1}
+ onSearch={q => Promise.resolve(branchList.filter(b => b.value.includes(q)))}
+ onSelect={option => props.onChangeReferenceBranch(option.value)}
+ renderOption={renderBranchOption}
+ value={currentBranch}
+ />
+ </div>
+ </>
+ )}
+ </>
+ </RadioCard>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
index 3d5388bdafb..6a451be1e02 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
@@ -17,22 +17,13 @@
* 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 classNames from 'classnames';
import { subDays } from 'date-fns';
import { throttle } from 'lodash';
import * as React from 'react';
-import Radio from 'sonar-ui-common/components/controls/Radio';
-import Select from 'sonar-ui-common/components/controls/Select';
-import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
-import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
-import TimeFormatter from 'sonar-ui-common/components/intl/TimeFormatter';
-import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { parseDate, toShortNotSoISOString } from 'sonar-ui-common/helpers/dates';
-import { translate } from 'sonar-ui-common/helpers/l10n';
import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
import { getProjectActivity } from '../../../api/projectActivity';
-import Events from '../../projectActivity/components/Events';
-import { getAnalysesByVersionByDay } from '../../projectActivity/utils';
+import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
interface Props {
analysis: string;
@@ -48,10 +39,12 @@ interface State {
scroll: number;
}
+const STICKY_BADGE_SCROLL_OFFSET = 10;
+
export default class BranchAnalysisList extends React.PureComponent<Props, State> {
mounted = false;
badges: T.Dict<HTMLDivElement> = {};
- rootNodeRef: React.RefObject<HTMLDivElement>;
+ scrollableNode?: HTMLDivElement;
state: State = {
analyses: [],
loading: true,
@@ -61,7 +54,6 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State
constructor(props: Props) {
super(props);
- this.rootNodeRef = React.createRef<HTMLDivElement>();
this.updateScroll = throttle(this.updateScroll, 20);
}
@@ -76,8 +68,8 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State
scrollToSelected() {
const selectedNode = document.querySelector('.branch-analysis.selected');
- if (this.rootNodeRef.current && selectedNode) {
- scrollToElement(selectedNode, { parent: this.rootNodeRef.current, bottomOffset: 40 });
+ if (this.scrollableNode && selectedNode) {
+ scrollToElement(selectedNode, { parent: this.scrollableNode, bottomOffset: 40 });
}
}
@@ -133,145 +125,35 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State
shouldStick = (version: string) => {
const badge = this.badges[version];
- return badge && Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + 10;
+ return (
+ !!badge &&
+ Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + STICKY_BADGE_SCROLL_OFFSET
+ );
};
- getRangeOptions() {
- return [
- {
- label: translate('baseline.branch_analyses.ranges.30days'),
- value: 30
- },
- {
- label: translate('baseline.branch_analyses.ranges.allTime'),
- value: 0
- }
- ];
- }
-
handleRangeChange = ({ value }: { value: number }) => {
this.setState({ range: value }, () => this.fetchAnalyses());
};
render() {
+ const { analysis, onSelectAnalysis } = this.props;
const { analyses, loading, range } = this.state;
- const byVersionByDay = getAnalysesByVersionByDay(analyses, {
- category: ''
- });
-
- const hasFilteredData =
- byVersionByDay.length > 1 ||
- (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
-
return (
- <>
- <div className="spacer-bottom">
- {translate('baseline.analysis_from')}
- <Select
- autoBlur={true}
- className="input-medium spacer-left"
- clearable={false}
- onChange={this.handleRangeChange}
- options={this.getRangeOptions()}
- searchable={false}
- value={range}
- />
- </div>
- <div className="branch-analysis-list-wrapper">
- <div
- className="bordered branch-analysis-list"
- onScroll={this.handleScroll}
- ref={this.rootNodeRef}>
- {loading && <DeferredSpinner className="big-spacer-top" />}
-
- {!loading && !hasFilteredData ? (
- <div className="big-spacer-top big-spacer-bottom strong">
- {translate('baseline.no_analyses')}
- </div>
- ) : (
- <ul>
- {byVersionByDay.map((version, idx) => {
- const days = Object.keys(version.byDay);
- if (days.length <= 0) {
- return null;
- }
- return (
- <li key={version.key || 'noversion'}>
- {version.version && (
- <div
- className={classNames('branch-analysis-version-badge', {
- first: idx === 0,
- sticky: this.shouldStick(version.version)
- })}
- ref={this.registerBadgeNode(version.version)}>
- <Tooltip
- mouseEnterDelay={0.5}
- overlay={`${translate('version')} ${version.version}`}>
- <span className="badge">{version.version}</span>
- </Tooltip>
- </div>
- )}
- <ul className="branch-analysis-days-list">
- {days.map(day => (
- <li
- className="branch-analysis-day"
- data-day={toShortNotSoISOString(Number(day))}
- key={day}>
- <div className="branch-analysis-date">
- <DateFormatter date={Number(day)} long={true} />
- </div>
- <ul className="branch-analysis-analyses-list">
- {version.byDay[day] != null &&
- version.byDay[day].map(analysis => (
- <li
- className={classNames('branch-analysis', {
- selected: analysis.key === this.props.analysis
- })}
- data-date={parseDate(analysis.date).valueOf()}
- key={analysis.key}
- onClick={() => this.props.onSelectAnalysis(analysis)}>
- <div className="branch-analysis-time spacer-right">
- <TimeFormatter date={parseDate(analysis.date)} long={false}>
- {formattedTime => (
- <time
- className="text-middle"
- dateTime={parseDate(analysis.date).toISOString()}>
- {formattedTime}
- </time>
- )}
- </TimeFormatter>
- </div>
-
- {analysis.events.length > 0 && (
- <Events
- analysisKey={analysis.key}
- events={analysis.events}
- isFirst={analyses[0].key === analysis.key}
- />
- )}
-
- <div className="analysis-selection-button">
- <Radio
- checked={analysis.key === this.props.analysis}
- onCheck={() => {}}
- value=""
- />
- </div>
- </li>
- ))}
- </ul>
- </li>
- ))}
- </ul>
- </li>
- );
- })}
- </ul>
- )}
- </div>
- </div>
- </>
+ <BranchAnalysisListRenderer
+ analyses={analyses}
+ handleRangeChange={this.handleRangeChange}
+ handleScroll={this.handleScroll}
+ loading={loading}
+ onSelectAnalysis={onSelectAnalysis}
+ range={range}
+ registerBadgeNode={this.registerBadgeNode}
+ registerScrollableNode={el => {
+ this.scrollableNode = el;
+ }}
+ selectedAnalysisKey={analysis}
+ shouldStick={this.shouldStick}
+ />
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx
new file mode 100644
index 00000000000..36dc680b742
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx
@@ -0,0 +1,186 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import Select from 'sonar-ui-common/components/controls/Select';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
+import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
+import TimeFormatter from 'sonar-ui-common/components/intl/TimeFormatter';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { parseDate, toShortNotSoISOString } from 'sonar-ui-common/helpers/dates';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import Events from '../../projectActivity/components/Events';
+import { getAnalysesByVersionByDay } from '../../projectActivity/utils';
+
+export interface BranchAnalysisListRendererProps {
+ analyses: T.ParsedAnalysis[];
+ handleRangeChange: ({ value }: { value: number }) => void;
+ handleScroll: (e: React.SyntheticEvent<HTMLDivElement>) => void;
+ loading: boolean;
+ onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
+ range: number;
+ registerBadgeNode: (version: string) => (el: HTMLDivElement) => void;
+ registerScrollableNode: (el: HTMLDivElement) => void;
+ selectedAnalysisKey: string;
+ shouldStick: (version: string) => boolean;
+}
+
+function renderAnalysis(args: {
+ analysis: T.ParsedAnalysis;
+ isFirst: boolean;
+ onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
+ selectedAnalysisKey: string;
+}) {
+ const { analysis, isFirst, onSelectAnalysis, selectedAnalysisKey } = args;
+ return (
+ <li
+ className={classNames('branch-analysis', {
+ selected: analysis.key === selectedAnalysisKey
+ })}
+ data-date={parseDate(analysis.date).valueOf()}
+ key={analysis.key}
+ onClick={() => onSelectAnalysis(analysis)}>
+ <div className="branch-analysis-time spacer-right">
+ <TimeFormatter date={parseDate(analysis.date)} long={false}>
+ {formattedTime => (
+ <time className="text-middle" dateTime={parseDate(analysis.date).toISOString()}>
+ {formattedTime}
+ </time>
+ )}
+ </TimeFormatter>
+ </div>
+
+ {analysis.events.length > 0 && (
+ <Events analysisKey={analysis.key} events={analysis.events} isFirst={isFirst} />
+ )}
+
+ <div className="analysis-selection-button">
+ <Radio checked={analysis.key === selectedAnalysisKey} onCheck={() => {}} value="" />
+ </div>
+ </li>
+ );
+}
+
+export default function BranchAnalysisListRenderer(props: BranchAnalysisListRendererProps) {
+ const { analyses, loading, range, selectedAnalysisKey } = props;
+
+ const byVersionByDay = React.useMemo(
+ () =>
+ getAnalysesByVersionByDay(analyses, {
+ category: ''
+ }),
+ [analyses]
+ );
+
+ const hasFilteredData =
+ byVersionByDay.length > 1 ||
+ (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
+
+ return (
+ <>
+ <div className="spacer-bottom">
+ {translate('baseline.analysis_from')}
+ <Select
+ autoBlur={true}
+ className="input-medium spacer-left"
+ clearable={false}
+ onChange={props.handleRangeChange}
+ options={[
+ {
+ label: translate('baseline.branch_analyses.ranges.30days'),
+ value: 30
+ },
+ {
+ label: translate('baseline.branch_analyses.ranges.allTime'),
+ value: 0
+ }
+ ]}
+ searchable={false}
+ value={range}
+ />
+ </div>
+ <div className="branch-analysis-list-wrapper">
+ <div
+ className="bordered branch-analysis-list"
+ onScroll={props.handleScroll}
+ ref={props.registerScrollableNode}>
+ {loading && <DeferredSpinner className="big-spacer-top" />}
+
+ {!loading && !hasFilteredData ? (
+ <div className="big-spacer-top big-spacer-bottom strong">
+ {translate('baseline.no_analyses')}
+ </div>
+ ) : (
+ <ul>
+ {byVersionByDay.map((version, idx) => {
+ const days = Object.keys(version.byDay);
+ if (days.length <= 0) {
+ return null;
+ }
+ return (
+ <li key={version.key || 'noversion'}>
+ {version.version && (
+ <div
+ className={classNames('branch-analysis-version-badge', {
+ first: idx === 0,
+ sticky: props.shouldStick(version.version)
+ })}
+ ref={props.registerBadgeNode(version.version)}>
+ <Tooltip
+ mouseEnterDelay={0.5}
+ overlay={`${translate('version')} ${version.version}`}>
+ <span className="badge">{version.version}</span>
+ </Tooltip>
+ </div>
+ )}
+ <ul className="branch-analysis-days-list">
+ {days.map(day => (
+ <li
+ className="branch-analysis-day"
+ data-day={toShortNotSoISOString(Number(day))}
+ key={day}>
+ <div className="branch-analysis-date">
+ <DateFormatter date={Number(day)} long={true} />
+ </div>
+ <ul className="branch-analysis-analyses-list">
+ {version.byDay[day] != null &&
+ version.byDay[day].map(analysis =>
+ renderAnalysis({
+ analysis,
+ selectedAnalysisKey,
+ isFirst: analyses[0].key === analysis.key,
+ onSelectAnalysis: props.onSelectAnalysis
+ })
+ )}
+ </ul>
+ </li>
+ ))}
+ </ul>
+ </li>
+ );
+ })}
+ </ul>
+ )}
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
index 6981942941e..a34749d52b3 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
@@ -24,15 +24,17 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { toNotSoISOString } from 'sonar-ui-common/helpers/dates';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { setNewCodePeriod } from '../../../api/newCodePeriod';
-import { BranchWithNewCodePeriod } from '../../../types/branch-like';
+import { Branch, BranchWithNewCodePeriod } from '../../../types/branch-like';
import { getSettingValue, validateSetting } from '../utils';
import BaselineSettingAnalysis from './BaselineSettingAnalysis';
import BaselineSettingDays from './BaselineSettingDays';
import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion';
+import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
import BranchAnalysisList from './BranchAnalysisList';
interface Props {
branch: BranchWithNewCodePeriod;
+ branchList: Branch[];
component: string;
onClose: (branch?: string, newSetting?: T.NewCodePeriod) => void;
}
@@ -41,6 +43,7 @@ interface State {
analysis: string;
analysisDate?: Date;
days: string;
+ referenceBranch: string;
saving: boolean;
selected?: T.NewCodePeriodSettingType;
}
@@ -51,9 +54,13 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
constructor(props: Props) {
super(props);
+ const otherBranches = props.branchList.filter(b => b.name !== props.branch.name);
+ const defaultBranch = otherBranches.length > 0 ? otherBranches[0].name : '';
+
this.state = {
analysis: this.getValueFromProps('SPECIFIC_ANALYSIS') || '',
days: this.getValueFromProps('NUMBER_OF_DAYS') || '30',
+ referenceBranch: this.getValueFromProps('REFERENCE_BRANCH') || defaultBranch,
saving: false,
selected: this.props.branch.newCodePeriod && this.props.branch.newCodePeriod.type
};
@@ -73,13 +80,19 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
: null;
}
+ branchToOption = (b: Branch) => ({
+ value: b.name,
+ isMain: b.isMain,
+ disabled: b.name === this.props.branch.name // cannot itself be used as a reference branch
+ });
+
handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
const { branch, component } = this.props;
- const { analysis, analysisDate, days, selected: type } = this.state;
+ const { analysis, analysisDate, days, referenceBranch, selected: type } = this.state;
- const value = getSettingValue({ type, analysis, days });
+ const value = getSettingValue({ type, analysis, days, referenceBranch });
if (type) {
this.setState({ saving: true });
@@ -115,11 +128,13 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
handleSelectDays = (days: string) => this.setState({ days });
+ handleSelectReferenceBranch = (referenceBranch: string) => this.setState({ referenceBranch });
+
handleSelectSetting = (selected: T.NewCodePeriodSettingType) => this.setState({ selected });
render() {
- const { branch } = this.props;
- const { analysis, days, saving, selected } = this.state;
+ const { branch, branchList } = this.props;
+ const { analysis, days, referenceBranch, saving, selected } = this.state;
const header = translateWithParameters('baseline.new_code_period_for_branch_x', branch.name);
@@ -131,6 +146,7 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
currentSetting,
currentSettingValue,
days,
+ referenceBranch,
selected
});
@@ -159,6 +175,14 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
onSelect={this.handleSelectSetting}
selected={selected === 'SPECIFIC_ANALYSIS'}
/>
+ <BaselineSettingReferenceBranch
+ branchList={branchList.map(this.branchToOption)}
+ onChangeReferenceBranch={this.handleSelectReferenceBranch}
+ onSelect={this.handleSelectSetting}
+ referenceBranch={referenceBranch}
+ selected={selected === 'REFERENCE_BRANCH'}
+ settingLevel="branch"
+ />
</div>
{selected === 'SPECIFIC_ANALYSIS' && (
<BranchAnalysisList
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
index 068fcce5f0a..dbfe9f7e7b5 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
@@ -18,20 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import ActionsDropdown, {
- ActionsDropdownItem
-} from 'sonar-ui-common/components/controls/ActionsDropdown';
-import DateTimeFormatter from 'sonar-ui-common/components/intl/DateTimeFormatter';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { listBranchesNewCodePeriod, resetNewCodePeriod } from '../../../api/newCodePeriod';
-import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
import { isBranch, sortBranches } from '../../../helpers/branch-like';
-import { BranchLike, BranchWithNewCodePeriod } from '../../../types/branch-like';
+import { Branch, BranchLike, BranchWithNewCodePeriod } from '../../../types/branch-like';
import BranchBaselineSettingModal from './BranchBaselineSettingModal';
+import BranchListRow from './BranchListRow';
interface Props {
- branchLikes: BranchLike[];
+ branchList: Branch[];
component: T.Component;
inheritedSetting: T.NewCodePeriod;
}
@@ -66,15 +62,13 @@ export default class BranchList extends React.PureComponent<Props, State> {
const project = this.props.component.key;
this.setState({ loading: true });
- const sortedBranches = this.sortAndFilterBranches(this.props.branchLikes);
-
listBranchesNewCodePeriod({ project }).then(
branchSettings => {
const newCodePeriods = branchSettings.newCodePeriods
? branchSettings.newCodePeriods.filter(ncp => !ncp.inherited)
: [];
- const branchesWithBaseline = sortedBranches.map(b => {
+ const branchesWithBaseline = this.props.branchList.map(b => {
const newCodePeriod = newCodePeriods.find(ncp => ncp.branchKey === b.name);
if (!newCodePeriod) {
return b;
@@ -119,38 +113,17 @@ export default class BranchList extends React.PureComponent<Props, State> {
}
};
- resetToDefault(branch: string) {
+ resetToDefault = (branch: string) => {
return resetNewCodePeriod({
project: this.props.component.key,
branch
}).then(() => {
this.setState({ branches: this.updateBranchNewCodePeriod(branch, undefined) });
});
- }
-
- renderNewCodePeriodSetting(newCodePeriod: T.NewCodePeriod) {
- switch (newCodePeriod.type) {
- case 'SPECIFIC_ANALYSIS':
- return (
- <>
- {`${translate('baseline.specific_analysis')}: `}
- {newCodePeriod.effectiveValue ? (
- <DateTimeFormatter date={newCodePeriod.effectiveValue} />
- ) : (
- '?'
- )}
- </>
- );
- case 'NUMBER_OF_DAYS':
- return `${translate('baseline.number_days')}: ${newCodePeriod.value}`;
- case 'PREVIOUS_VERSION':
- return translate('baseline.previous_version');
- default:
- return newCodePeriod.type;
- }
- }
+ };
render() {
+ const { branchList, inheritedSetting } = this.props;
const { branches, editedBranch, loading } = this.state;
if (branches.length < 1) {
@@ -175,38 +148,21 @@ export default class BranchList extends React.PureComponent<Props, State> {
</thead>
<tbody>
{branches.map(branch => (
- <tr key={branch.name}>
- <td className="nowrap">
- <BranchLikeIcon branchLike={branch} className="little-spacer-right" />
- {branch.name}
- {branch.isMain && (
- <div className="badge spacer-left">{translate('branches.main_branch')}</div>
- )}
- </td>
- <td className="huge-spacer-right nowrap">
- {branch.newCodePeriod
- ? this.renderNewCodePeriodSetting(branch.newCodePeriod)
- : translate('branch_list.default_setting')}
- </td>
- <td className="text-right">
- <ActionsDropdown>
- <ActionsDropdownItem onClick={() => this.openEditModal(branch)}>
- {translate('edit')}
- </ActionsDropdownItem>
- {branch.newCodePeriod && (
- <ActionsDropdownItem onClick={() => this.resetToDefault(branch.name)}>
- {translate('reset_to_default')}
- </ActionsDropdownItem>
- )}
- </ActionsDropdown>
- </td>
- </tr>
+ <BranchListRow
+ branch={branch}
+ existingBranches={branchList.map(b => b.name)}
+ inheritedSetting={inheritedSetting}
+ key={branch.name}
+ onOpenEditModal={this.openEditModal}
+ onResetToDefault={this.resetToDefault}
+ />
))}
</tbody>
</table>
{editedBranch && (
<BranchBaselineSettingModal
branch={editedBranch}
+ branchList={branchList}
component={this.props.component.key}
onClose={this.closeEditModal}
/>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
new file mode 100644
index 00000000000..7cfc4b426f5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import ActionsDropdown, {
+ ActionsDropdownItem
+} from 'sonar-ui-common/components/controls/ActionsDropdown';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
+import WarningIcon from 'sonar-ui-common/components/icons/WarningIcon';
+import DateTimeFormatter from 'sonar-ui-common/components/intl/DateTimeFormatter';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
+import { BranchWithNewCodePeriod } from '../../../types/branch-like';
+
+export interface BranchListRowProps {
+ branch: BranchWithNewCodePeriod;
+ existingBranches: Array<string>;
+ inheritedSetting: T.NewCodePeriod;
+ onOpenEditModal: (branch: BranchWithNewCodePeriod) => void;
+ onResetToDefault: (branchName: string) => void;
+}
+
+function renderNewCodePeriodSetting(newCodePeriod: T.NewCodePeriod) {
+ switch (newCodePeriod.type) {
+ case 'SPECIFIC_ANALYSIS':
+ return (
+ <>
+ {`${translate('baseline.specific_analysis')}: `}
+ {newCodePeriod.effectiveValue ? (
+ <DateTimeFormatter date={newCodePeriod.effectiveValue} />
+ ) : (
+ '?'
+ )}
+ </>
+ );
+ case 'NUMBER_OF_DAYS':
+ return `${translate('baseline.number_days')}: ${newCodePeriod.value}`;
+ case 'PREVIOUS_VERSION':
+ return translate('baseline.previous_version');
+ case 'REFERENCE_BRANCH':
+ return `${translate('baseline.reference_branch')}: ${newCodePeriod.value}`;
+ default:
+ return newCodePeriod.type;
+ }
+}
+
+function branchInheritsItselfAsReference(
+ branch: BranchWithNewCodePeriod,
+ inheritedSetting: T.NewCodePeriod
+) {
+ return (
+ !branch.newCodePeriod &&
+ inheritedSetting.type === 'REFERENCE_BRANCH' &&
+ branch.name === inheritedSetting.value
+ );
+}
+
+function referenceBranchDoesNotExist(
+ branch: BranchWithNewCodePeriod,
+ existingBranches: Array<string>
+) {
+ return (
+ branch.newCodePeriod &&
+ branch.newCodePeriod.value &&
+ branch.newCodePeriod.type === 'REFERENCE_BRANCH' &&
+ !existingBranches.includes(branch.newCodePeriod.value)
+ );
+}
+
+export default function BranchListRow(props: BranchListRowProps) {
+ const { branch, existingBranches, inheritedSetting } = props;
+
+ let settingWarning: string | undefined;
+ if (branchInheritsItselfAsReference(branch, inheritedSetting)) {
+ settingWarning = translateWithParameters(
+ 'baseline.reference_branch.invalid_branch_setting',
+ branch.name
+ );
+ } else if (referenceBranchDoesNotExist(branch, existingBranches)) {
+ settingWarning = translateWithParameters(
+ 'baseline.reference_branch.does_not_exist',
+ branch.newCodePeriod?.value || ''
+ );
+ }
+
+ return (
+ <tr className={settingWarning ? 'branch-setting-warning' : ''}>
+ <td className="nowrap">
+ <BranchLikeIcon branchLike={branch} className="little-spacer-right" />
+ {branch.name}
+ {branch.isMain && (
+ <div className="badge spacer-left">{translate('branches.main_branch')}</div>
+ )}
+ </td>
+ <td className="huge-spacer-right nowrap">
+ <Tooltip overlay={settingWarning}>
+ <span>
+ {settingWarning && <WarningIcon className="little-spacer-right" />}
+ {branch.newCodePeriod
+ ? renderNewCodePeriodSetting(branch.newCodePeriod)
+ : translate('branch_list.default_setting')}
+ </span>
+ </Tooltip>
+ </td>
+ <td className="text-right">
+ <ActionsDropdown>
+ <ActionsDropdownItem onClick={() => props.onOpenEditModal(branch)}>
+ {translate('edit')}
+ </ActionsDropdownItem>
+ {branch.newCodePeriod && (
+ <ActionsDropdownItem onClick={() => props.onResetToDefault(branch.name)}>
+ {translate('reset_to_default')}
+ </ActionsDropdownItem>
+ )}
+ </ActionsDropdown>
+ </td>
+ </tr>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
index 9345526b226..41bcab83178 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
@@ -21,16 +21,20 @@ import * as classNames from 'classnames';
import * as React from 'react';
import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Radio from 'sonar-ui-common/components/controls/Radio';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { Branch } from '../../../types/branch-like';
import { validateSetting } from '../utils';
import BaselineSettingAnalysis from './BaselineSettingAnalysis';
import BaselineSettingDays from './BaselineSettingDays';
import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion';
+import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
import BranchAnalysisList from './BranchAnalysisList';
export interface ProjectBaselineSelectorProps {
analysis?: string;
+ branchList: Branch[];
branchesEnabled?: boolean;
component: string;
currentSetting?: T.NewCodePeriodSettingType;
@@ -40,9 +44,11 @@ export interface ProjectBaselineSelectorProps {
onCancel: () => void;
onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
onSelectDays: (value: string) => void;
+ onSelectReferenceBranch: (value: string) => void;
onSelectSetting: (value?: T.NewCodePeriodSettingType) => void;
onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void;
onToggleSpecificSetting: (selection: boolean) => void;
+ referenceBranch?: string;
saving: boolean;
selected?: T.NewCodePeriodSettingType;
overrideGeneralSetting: boolean;
@@ -69,18 +75,24 @@ function renderGeneralSetting(generalSetting: T.NewCodePeriod) {
);
}
+function branchToOption(b: Branch) {
+ return { value: b.name, isMain: b.isMain };
+}
+
export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
const {
analysis,
+ branchList,
branchesEnabled,
component,
currentSetting,
currentSettingValue,
days,
generalSetting,
+ overrideGeneralSetting,
+ referenceBranch,
saving,
- selected,
- overrideGeneralSetting
+ selected
} = props;
const { isChanged, isValid } = validateSetting({
@@ -88,8 +100,9 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
currentSetting,
currentSettingValue,
days,
- selected,
- overrideGeneralSetting
+ overrideGeneralSetting,
+ referenceBranch,
+ selected
});
return (
@@ -113,7 +126,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
</Radio>
</div>
- <div className="big-spacer-left big-spacer-right branch-baseline-setting-modal">
+ <div className="big-spacer-left big-spacer-right project-baseline-setting">
<div className="display-flex-row big-spacer-bottom" role="radiogroup">
<BaselineSettingPreviousVersion
disabled={!overrideGeneralSetting}
@@ -129,7 +142,17 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
onSelect={props.onSelectSetting}
selected={overrideGeneralSetting && selected === 'NUMBER_OF_DAYS'}
/>
- {!branchesEnabled && (
+ {branchesEnabled ? (
+ <BaselineSettingReferenceBranch
+ branchList={branchList.map(branchToOption)}
+ disabled={!overrideGeneralSetting}
+ onChangeReferenceBranch={props.onSelectReferenceBranch}
+ onSelect={props.onSelectSetting}
+ referenceBranch={referenceBranch || ''}
+ selected={overrideGeneralSetting && selected === 'REFERENCE_BRANCH'}
+ settingLevel="project"
+ />
+ ) : (
<BaselineSettingAnalysis
disabled={!overrideGeneralSetting}
onSelect={props.onSelectSetting}
@@ -147,7 +170,9 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
)}
</div>
<div className={classNames('big-spacer-top', { invisible: !isChanged })}>
- <p className="spacer-bottom">{translate('baseline.next_analysis_notice')}</p>
+ <Alert variant="info" className="spacer-bottom">
+ {translate('baseline.next_analysis_notice')}
+ </Alert>
<DeferredSpinner className="spacer-right" loading={saving} />
<SubmitButton disabled={saving || !isValid || !isChanged}>{translate('save')}</SubmitButton>
<ResetButtonLink className="spacer-left" onClick={props.onCancel}>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
index 45e36d3472d..a46c17a1128 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
@@ -25,6 +25,7 @@ import {
resetNewCodePeriod,
setNewCodePeriod
} from '../../../../api/newCodePeriod';
+import { mockBranch, mockMainBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like';
import { mockComponent, mockEvent } from '../../../../helpers/testMocks';
import App from '../App';
@@ -38,6 +39,16 @@ it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});
+it('should initialize correctly', async () => {
+ const wrapper = shallowRender({
+ branchLikes: [mockBranch(), mockPullRequest(), mockMainBranch()]
+ });
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().branchList).toHaveLength(2);
+ expect(wrapper.state().referenceBranch).toBe('master');
+});
+
it('should not display reset button if project setting is not set', () => {
const wrapper = shallowRender();
@@ -89,7 +100,7 @@ it('should handle errors gracefully', async () => {
function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(
<App
- branchLikes={[]}
+ branchLikes={[mockMainBranch()]}
branchesEnabled={true}
canAdmin={true}
component={mockComponent()}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/AppHeader-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/AppHeader-test.tsx
new file mode 100644
index 00000000000..5e54700d6ce
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/AppHeader-test.tsx
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import AppHeader, { AppHeaderProps } from '../AppHeader';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('can admin');
+ expect(shallowRender({ canAdmin: false })).toMatchSnapshot('cannot admin');
+});
+
+function shallowRender(props: Partial<AppHeaderProps> = {}) {
+ return shallow(<AppHeader canAdmin={true} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx
new file mode 100644
index 00000000000..027ea375e1f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx
@@ -0,0 +1,117 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import RadioCard from 'sonar-ui-common/components/controls/RadioCard';
+import SearchSelect from 'sonar-ui-common/components/controls/SearchSelect';
+import BaselineSettingReferenceBranch, {
+ BaselineSettingReferenceBranchProps,
+ BranchOption
+} from '../BaselineSettingReferenceBranch';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('Project level');
+ expect(shallowRender({ settingLevel: 'branch', configuredBranchName: 'master' })).toMatchSnapshot(
+ 'Branch level'
+ );
+ expect(
+ shallowRender({
+ branchList: [{ value: 'master', isMain: true }],
+ settingLevel: 'branch',
+ configuredBranchName: 'master'
+ })
+ ).toMatchSnapshot('Branch level - no other branches');
+});
+
+it('should not display input when not selected', () => {
+ const wrapper = shallowRender({ selected: false });
+ expect(wrapper.find('SearchSelect')).toHaveLength(0);
+});
+
+it('should callback when clicked', () => {
+ const onSelect = jest.fn();
+ const wrapper = shallowRender({ onSelect, selected: false });
+
+ wrapper
+ .find(RadioCard)
+ .first()
+ .simulate('click');
+ expect(onSelect).toHaveBeenCalledWith('REFERENCE_BRANCH');
+});
+
+it('should callback when changing selection', () => {
+ const onChangeReferenceBranch = jest.fn();
+ const wrapper = shallowRender({ onChangeReferenceBranch });
+
+ wrapper
+ .find(SearchSelect)
+ .first()
+ .simulate('select', { value: 'branch-6.9' });
+ expect(onChangeReferenceBranch).toHaveBeenCalledWith('branch-6.9');
+});
+
+it('should handle an invalid branch', () => {
+ const unknownBranchName = 'branch-unknown';
+ const wrapper = shallowRender({ referenceBranch: unknownBranchName });
+
+ expect(
+ wrapper
+ .find(SearchSelect)
+ .first()
+ .props().value
+ ).toEqual({ value: unknownBranchName, isMain: false, isInvalid: true });
+});
+
+describe('renderOption', () => {
+ const select = shallowRender()
+ .find(SearchSelect)
+ .first();
+ const renderFunction = select.props().renderOption as (option: BranchOption) => JSX.Element;
+
+ it('should render correctly', () => {
+ expect(renderFunction({ value: 'master', isMain: true })).toMatchSnapshot('main');
+ expect(renderFunction({ value: 'branch-7.4', isMain: false })).toMatchSnapshot('branch');
+ expect(renderFunction({ value: 'disabled', isMain: false, disabled: true })).toMatchSnapshot(
+ 'disabled'
+ );
+ expect(
+ renderFunction({ value: 'branch-nope', isMain: false, isInvalid: true })
+ ).toMatchSnapshot("branch doesn't exist");
+ });
+});
+
+function shallowRender(props: Partial<BaselineSettingReferenceBranchProps> = {}) {
+ const branchOptions = [
+ { value: 'master', isMain: true },
+ { value: 'branch-7.9', isMain: false }
+ ];
+
+ return shallow(
+ <BaselineSettingReferenceBranch
+ branchList={branchOptions}
+ settingLevel="project"
+ onChangeReferenceBranch={jest.fn()}
+ onSelect={jest.fn()}
+ referenceBranch="master"
+ selected={true}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisList-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisList-test.tsx
index c3701878f83..f9dc0e12c30 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisList-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisList-test.tsx
@@ -76,7 +76,8 @@ it('should render correctly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
+ expect(getProjectActivity).toBeCalled();
+ expect(wrapper.state().analyses).toHaveLength(4);
});
it('should reload analyses after range change', () => {
@@ -109,6 +110,22 @@ it('should handle scroll', () => {
expect(wrapper.state('scroll')).toBe(12);
});
+describe('shouldStick', () => {
+ const wrapper = shallowRender();
+
+ wrapper.instance().badges['10.5'] = mockBadge('43');
+ wrapper.instance().badges['12.2'] = mockBadge('85');
+
+ it('should handle no badge', () => {
+ expect(wrapper.instance().shouldStick('unknown version')).toBe(false);
+ });
+ it('should return the correct result', () => {
+ wrapper.setState({ scroll: 36 }); // => 46 with STICKY_BADGE_SCROLL_OFFSET = 10
+ expect(wrapper.instance().shouldStick('10.5')).toBe(true);
+ expect(wrapper.instance().shouldStick('12.2')).toBe(false);
+ });
+});
+
function shallowRender(props: Partial<BranchAnalysisList['props']> = {}) {
return shallow<BranchAnalysisList>(
<BranchAnalysisList
@@ -120,3 +137,11 @@ function shallowRender(props: Partial<BranchAnalysisList['props']> = {}) {
/>
);
}
+
+function mockBadge(offsetTop: string) {
+ const element = document.createElement('div');
+
+ element.setAttribute('originOffsetTop', offsetTop);
+
+ return element;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisListRenderer-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisListRenderer-test.tsx
new file mode 100644
index 00000000000..e610a98521f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisListRenderer-test.tsx
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockAnalysisEvent, mockParsedAnalysis } from '../../../../helpers/testMocks';
+import BranchAnalysisListRenderer, {
+ BranchAnalysisListRendererProps
+} from '../BranchAnalysisListRenderer';
+
+jest.mock('date-fns/start_of_day', () => (date: Date) => {
+ const startDay = new Date(date);
+ startDay.setUTCHours(0, 0, 0, 0);
+ return startDay;
+});
+
+jest.mock('sonar-ui-common/helpers/dates', () => {
+ const actual = require.requireActual('sonar-ui-common/helpers/dates');
+ return { ...actual, toShortNotSoISOString: (date: string) => `ISO.${date}` };
+});
+
+const analyses = [
+ mockParsedAnalysis({
+ key: '4',
+ date: new Date('2017-03-02T10:36:01Z'),
+ projectVersion: '4.2'
+ }),
+ mockParsedAnalysis({
+ key: '3',
+ date: new Date('2017-03-02T09:36:01Z'),
+ events: [mockAnalysisEvent()],
+ projectVersion: '4.2'
+ }),
+ mockParsedAnalysis({
+ key: '2',
+ date: new Date('2017-03-02T08:36:01Z'),
+ events: [
+ mockAnalysisEvent(),
+ mockAnalysisEvent({ category: 'VERSION', qualityGate: undefined })
+ ],
+ projectVersion: '4.1'
+ }),
+ mockParsedAnalysis({ key: '1', projectVersion: '4.1' })
+];
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('empty');
+ expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+ expect(shallowRender({ analyses, selectedAnalysisKey: '2' })).toMatchSnapshot('Analyses');
+});
+
+function shallowRender(props: Partial<BranchAnalysisListRendererProps> = {}) {
+ return shallow(
+ <BranchAnalysisListRenderer
+ analyses={[]}
+ handleRangeChange={jest.fn()}
+ handleScroll={jest.fn()}
+ loading={false}
+ onSelectAnalysis={jest.fn()}
+ range={30}
+ registerBadgeNode={jest.fn()}
+ registerScrollableNode={jest.fn()}
+ selectedAnalysisKey=""
+ shouldStick={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx
index 7537c885118..1974b0e217b 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { setNewCodePeriod } from '../../../../api/newCodePeriod';
-import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
+import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
import BranchBaselineSettingModal from '../BranchBaselineSettingModal';
jest.mock('../../../../api/newCodePeriod', () => ({
@@ -29,7 +29,10 @@ jest.mock('../../../../api/newCodePeriod', () => ({
}));
it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot('only one branch');
+ expect(
+ shallowRender({ branchList: [mockMainBranch(), mockBranch()], branch: mockMainBranch() })
+ ).toMatchSnapshot('multiple branches');
});
it('should display the branch analysis list when necessary', () => {
@@ -92,6 +95,7 @@ function shallowRender(props: Partial<BranchBaselineSettingModal['props']> = {})
return shallow<BranchBaselineSettingModal>(
<BranchBaselineSettingModal
branch={mockMainBranch()}
+ branchList={[mockMainBranch()]}
component="compKey"
onClose={jest.fn()}
{...props}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx
index 70c6b10b124..ad1536a8d5b 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { listBranchesNewCodePeriod, resetNewCodePeriod } from '../../../../api/newCodePeriod';
-import { mockBranch, mockMainBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like';
+import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/testMocks';
import BranchBaselineSettingModal from '../BranchBaselineSettingModal';
import BranchList from '../BranchList';
@@ -31,24 +31,19 @@ jest.mock('../../../../api/newCodePeriod', () => ({
resetNewCodePeriod: jest.fn().mockResolvedValue(null)
}));
+const newCodePeriods = [
+ {
+ projectKey: '',
+ branchKey: 'master',
+ type: 'NUMBER_OF_DAYS',
+ value: '27'
+ }
+];
+
it('should render correctly', async () => {
- (listBranchesNewCodePeriod as jest.Mock).mockResolvedValueOnce({
- newCodePeriods: [
- {
- projectKey: '',
- branchKey: 'master',
- type: 'NUMBER_OF_DAYS',
- value: '27'
- }
- ]
- });
+ (listBranchesNewCodePeriod as jest.Mock).mockResolvedValueOnce({ newCodePeriods });
const wrapper = shallowRender({
- branchLikes: [
- mockMainBranch(),
- mockBranch(),
- mockBranch({ name: 'branch-7.0' }),
- mockPullRequest()
- ]
+ branchList: [mockMainBranch(), mockBranch(), mockBranch({ name: 'branch-7.0' })]
});
await waitAndUpdate(wrapper);
expect(wrapper.state().branches).toHaveLength(3);
@@ -68,7 +63,7 @@ it('should handle reset', () => {
});
it('should toggle popup', async () => {
- const wrapper = shallowRender({ branchLikes: [mockMainBranch(), mockBranch()] });
+ const wrapper = shallowRender({ branchList: [mockMainBranch(), mockBranch()] });
wrapper.setState({ editedBranch: mockMainBranch() });
@@ -93,35 +88,10 @@ it('should toggle popup', async () => {
});
});
-it('should render the right setting label', () => {
- const wrapper = shallowRender();
-
- expect(
- wrapper.instance().renderNewCodePeriodSetting({ type: 'NUMBER_OF_DAYS', value: '21' })
- ).toBe('baseline.number_days: 21');
- expect(wrapper.instance().renderNewCodePeriodSetting({ type: 'PREVIOUS_VERSION' })).toBe(
- 'baseline.previous_version'
- );
- expect(
- wrapper.instance().renderNewCodePeriodSetting({
- type: 'SPECIFIC_ANALYSIS',
- value: 'A85835',
- effectiveValue: '2018-12-02T13:01:12'
- })
- ).toMatchInlineSnapshot(`
- <React.Fragment>
- baseline.specific_analysis:
- <DateTimeFormatter
- date="2018-12-02T13:01:12"
- />
- </React.Fragment>
- `);
-});
-
function shallowRender(props: Partial<BranchList['props']> = {}) {
return shallow<BranchList>(
<BranchList
- branchLikes={[]}
+ branchList={[]}
component={mockComponent()}
inheritedSetting={{ type: 'PREVIOUS_VERSION' }}
{...props}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx
new file mode 100644
index 00000000000..68c344ca057
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { ActionsDropdownItem } from 'sonar-ui-common/components/controls/ActionsDropdown';
+import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
+import BranchListRow, { BranchListRowProps } from '../BranchListRow';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('main branch with default');
+ expect(
+ shallowRender({
+ branch: mockBranch({ name: 'branch-7.3' }),
+ inheritedSetting: { type: 'REFERENCE_BRANCH', value: 'branch-7.3' }
+ })
+ ).toMatchSnapshot('faulty branch');
+ expect(
+ shallowRender({
+ branch: { ...mockBranch(), newCodePeriod: { type: 'NUMBER_OF_DAYS', value: '21' } }
+ })
+ ).toMatchSnapshot('branch with number of days');
+ expect(
+ shallowRender({
+ branch: { ...mockBranch(), newCodePeriod: { type: 'PREVIOUS_VERSION' } }
+ })
+ ).toMatchSnapshot('branch with previous version');
+ expect(
+ shallowRender({
+ branch: {
+ ...mockBranch(),
+ newCodePeriod: {
+ type: 'SPECIFIC_ANALYSIS',
+ value: 'A85835',
+ effectiveValue: '2018-12-02T13:01:12'
+ }
+ }
+ })
+ ).toMatchSnapshot('branch with specific analysis');
+ expect(
+ shallowRender({
+ branch: { ...mockBranch(), newCodePeriod: { type: 'REFERENCE_BRANCH', value: 'master' } }
+ })
+ ).toMatchSnapshot('branch with reference branch');
+});
+
+it('should callback to open modal when clicked', () => {
+ const openEditModal = jest.fn();
+ const branch = mockBranch();
+ const wrapper = shallowRender({ branch, onOpenEditModal: openEditModal });
+
+ wrapper
+ .find(ActionsDropdownItem)
+ .first()
+ .simulate('click');
+
+ expect(openEditModal).toBeCalledWith(branch);
+});
+
+it('should callback to reset when clicked', () => {
+ const resetToDefault = jest.fn();
+ const branchName = 'branch-6.5';
+ const wrapper = shallowRender({
+ branch: { ...mockBranch({ name: branchName }), newCodePeriod: { type: 'REFERENCE_BRANCH' } },
+ onResetToDefault: resetToDefault
+ });
+
+ wrapper
+ .find(ActionsDropdownItem)
+ .at(1)
+ .simulate('click');
+
+ expect(resetToDefault).toBeCalledWith(branchName);
+});
+
+function shallowRender(props: Partial<BranchListRowProps> = {}) {
+ return shallow(
+ <BranchListRow
+ branch={mockMainBranch()}
+ existingBranches={['master']}
+ inheritedSetting={{}}
+ onOpenEditModal={jest.fn()}
+ onResetToDefault={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
index 92ddd75c2ae..6a3a71b7a91 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
@@ -19,6 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import ProjectBaselineSelector, { ProjectBaselineSelectorProps } from '../ProjectBaselineSelector';
it('should render correctly', () => {
@@ -104,6 +105,7 @@ it('should disable the save button when date is invalid', () => {
function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) {
return shallow(
<ProjectBaselineSelector
+ branchList={[mockMainBranch()]}
branchesEnabled={true}
component=""
days="12"
@@ -111,10 +113,12 @@ function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) {
onCancel={jest.fn()}
onSelectAnalysis={jest.fn()}
onSelectDays={jest.fn()}
+ onSelectReferenceBranch={jest.fn()}
onSelectSetting={jest.fn()}
onSubmit={jest.fn()}
onToggleSpecificSetting={jest.fn()}
overrideGeneralSetting={false}
+ referenceBranch="master"
saving={false}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
index d23ae1223a0..7f16b1b42d1 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -8,50 +8,9 @@ exports[`should render correctly 1`] = `
<div
className="page page-limited"
>
- <header
- className="page-header"
- >
- <h1
- className="page-title"
- >
- project_baseline.page
- </h1>
- <p
- className="page-description"
- >
- <FormattedMessage
- defaultMessage="project_baseline.page.description"
- id="project_baseline.page.description"
- values={
- Object {
- "link": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/documentation/project-administration/new-code-period/"
- >
- project_baseline.page.description.link
- </Link>,
- }
- }
- />
- <br />
- <FormattedMessage
- defaultMessage="project_baseline.page.description2"
- id="project_baseline.page.description2"
- values={
- Object {
- "link": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/admin/settings?category=new_code_period"
- >
- project_baseline.page.description2.link
- </Link>,
- }
- }
- />
- </p>
- </header>
+ <AppHeader
+ canAdmin={true}
+ />
<DeferredSpinner
timeout={100}
/>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap
new file mode 100644
index 00000000000..e698388bcc1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: can admin 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ project_baseline.page
+ </h1>
+ <p
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="project_baseline.page.description"
+ id="project_baseline.page.description"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/documentation/project-administration/new-code-period/"
+ >
+ project_baseline.page.description.link
+ </Link>,
+ }
+ }
+ />
+ <br />
+ <FormattedMessage
+ defaultMessage="project_baseline.page.description2"
+ id="project_baseline.page.description2"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/admin/settings?category=new_code_period"
+ >
+ project_baseline.page.description2.link
+ </Link>,
+ }
+ }
+ />
+ </p>
+</header>
+`;
+
+exports[`should render correctly: cannot admin 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ project_baseline.page
+ </h1>
+ <p
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="project_baseline.page.description"
+ id="project_baseline.page.description"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/documentation/project-administration/new-code-period/"
+ >
+ project_baseline.page.description.link
+ </Link>,
+ }
+ }
+ />
+ <br />
+ </p>
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap
new file mode 100644
index 00000000000..c7478b0de43
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap
@@ -0,0 +1,210 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renderOption should render correctly: branch 1`] = `
+<React.Fragment>
+ <span>
+ branch-7.4
+ </span>
+</React.Fragment>
+`;
+
+exports[`renderOption should render correctly: branch doesn't exist 1`] = `
+<Tooltip
+ overlay="baseline.reference_branch.does_not_exist.branch-nope"
+>
+ <span>
+ branch-nope
+
+ <AlertErrorIcon />
+ </span>
+</Tooltip>
+`;
+
+exports[`renderOption should render correctly: disabled 1`] = `
+<React.Fragment>
+ <span
+ title="baseline.reference_branch.cannot_be_itself"
+ >
+ disabled
+ </span>
+</React.Fragment>
+`;
+
+exports[`renderOption should render correctly: main 1`] = `
+<React.Fragment>
+ <span>
+ master
+ </span>
+ <div
+ className="badge spacer-left"
+ >
+ branches.main_branch
+ </div>
+</React.Fragment>
+`;
+
+exports[`should render correctly: Branch level - no other branches 1`] = `
+<RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="baseline.reference_branch"
+>
+ <p>
+ baseline.reference_branch.description
+ </p>
+ <div
+ className="big-spacer-top display-flex-column"
+ >
+ <label
+ className="text-middle"
+ htmlFor="reference_branch"
+ >
+ <strong>
+ baseline.reference_branch.choose
+ </strong>
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <SearchSelect
+ autofocus={false}
+ className="little-spacer-top spacer-bottom"
+ defaultOptions={
+ Array [
+ Object {
+ "isMain": true,
+ "value": "master",
+ },
+ ]
+ }
+ minimumQueryLength={1}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]}
+ value={
+ Object {
+ "isMain": true,
+ "value": "master",
+ }
+ }
+ />
+ </div>
+</RadioCard>
+`;
+
+exports[`should render correctly: Branch level 1`] = `
+<RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="baseline.reference_branch"
+>
+ <p>
+ baseline.reference_branch.description
+ </p>
+ <div
+ className="big-spacer-top display-flex-column"
+ >
+ <label
+ className="text-middle"
+ htmlFor="reference_branch"
+ >
+ <strong>
+ baseline.reference_branch.choose
+ </strong>
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <SearchSelect
+ autofocus={false}
+ className="little-spacer-top spacer-bottom"
+ defaultOptions={
+ Array [
+ Object {
+ "isMain": true,
+ "value": "master",
+ },
+ Object {
+ "isMain": false,
+ "value": "branch-7.9",
+ },
+ ]
+ }
+ minimumQueryLength={1}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]}
+ value={
+ Object {
+ "isMain": true,
+ "value": "master",
+ }
+ }
+ />
+ </div>
+</RadioCard>
+`;
+
+exports[`should render correctly: Project level 1`] = `
+<RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="baseline.reference_branch"
+>
+ <p>
+ baseline.reference_branch.description
+ </p>
+ <p
+ className="spacer-top"
+ >
+ baseline.reference_branch.description2
+ </p>
+ <div
+ className="big-spacer-top display-flex-column"
+ >
+ <label
+ className="text-middle"
+ htmlFor="reference_branch"
+ >
+ <strong>
+ baseline.reference_branch.choose
+ </strong>
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <SearchSelect
+ autofocus={false}
+ className="little-spacer-top spacer-bottom"
+ defaultOptions={
+ Array [
+ Object {
+ "isMain": true,
+ "value": "master",
+ },
+ Object {
+ "isMain": false,
+ "value": "branch-7.9",
+ },
+ ]
+ }
+ minimumQueryLength={1}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]}
+ value={
+ Object {
+ "isMain": true,
+ "value": "master",
+ }
+ }
+ />
+ </div>
+</RadioCard>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisListRenderer-test.tsx.snap
index 6bdc18ba481..9fdebdfdac8 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisListRenderer-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly 1`] = `
+exports[`should render correctly: Analyses 1`] = `
<Fragment>
<div
className="spacer-bottom"
@@ -10,7 +10,7 @@ exports[`should render correctly 1`] = `
autoBlur={true}
className="input-medium spacer-left"
clearable={false}
- onChange={[Function]}
+ onChange={[MockFunction]}
options={
Array [
Object {
@@ -24,7 +24,7 @@ exports[`should render correctly 1`] = `
]
}
searchable={false}
- value={0}
+ value={30}
/>
</div>
<div
@@ -32,7 +32,7 @@ exports[`should render correctly 1`] = `
>
<div
className="bordered branch-analysis-list"
- onScroll={[Function]}
+ onScroll={[MockFunction]}
>
<ul>
<li
@@ -43,14 +43,14 @@ exports[`should render correctly 1`] = `
>
<li
className="branch-analysis-day"
- data-day="2017-03-02"
- key="1488322800000"
+ data-day="ISO.1488412800000"
+ key="1488412800000"
>
<div
className="branch-analysis-date"
>
<DateFormatter
- date={1488322800000}
+ date={1488412800000}
long={true}
/>
</div>
@@ -59,7 +59,7 @@ exports[`should render correctly 1`] = `
>
<li
className="branch-analysis"
- data-date="2017-03-02"
+ data-date={1488450961000}
key="4"
onClick={[Function]}
>
@@ -67,7 +67,7 @@ exports[`should render correctly 1`] = `
className="branch-analysis-time spacer-right"
>
<TimeFormatter
- date="2017-03-02"
+ date={2017-03-02T10:36:01.000Z}
long={false}
>
<Component />
@@ -85,7 +85,7 @@ exports[`should render correctly 1`] = `
</li>
<li
className="branch-analysis"
- data-date="2017-03-02"
+ data-date={1488447361000}
key="3"
onClick={[Function]}
>
@@ -93,7 +93,7 @@ exports[`should render correctly 1`] = `
className="branch-analysis-time spacer-right"
>
<TimeFormatter
- date="2017-03-02"
+ date={2017-03-02T09:36:01.000Z}
long={false}
>
<Component />
@@ -165,14 +165,14 @@ exports[`should render correctly 1`] = `
>
<li
className="branch-analysis-day"
- data-day="2017-03-02"
- key="1488322800000"
+ data-day="ISO.1488412800000"
+ key="1488412800000"
>
<div
className="branch-analysis-date"
>
<DateFormatter
- date={1488322800000}
+ date={1488412800000}
long={true}
/>
</div>
@@ -180,8 +180,8 @@ exports[`should render correctly 1`] = `
className="branch-analysis-analyses-list"
>
<li
- className="branch-analysis"
- data-date="2017-03-02"
+ className="branch-analysis selected"
+ data-date={1488443761000}
key="2"
onClick={[Function]}
>
@@ -189,7 +189,7 @@ exports[`should render correctly 1`] = `
className="branch-analysis-time spacer-right"
>
<TimeFormatter
- date="2017-03-02"
+ date={2017-03-02T08:36:01.000Z}
long={false}
>
<Component />
@@ -236,15 +236,33 @@ exports[`should render correctly 1`] = `
className="analysis-selection-button"
>
<Radio
- checked={false}
+ checked={true}
onCheck={[Function]}
value=""
/>
</div>
</li>
+ </ul>
+ </li>
+ <li
+ className="branch-analysis-day"
+ data-day="ISO.1488326400000"
+ key="1488326400000"
+ >
+ <div
+ className="branch-analysis-date"
+ >
+ <DateFormatter
+ date={1488326400000}
+ long={true}
+ />
+ </div>
+ <ul
+ className="branch-analysis-analyses-list"
+ >
<li
className="branch-analysis"
- data-date="2017-03-02"
+ data-date={1488357421000}
key="1"
onClick={[Function]}
>
@@ -252,7 +270,7 @@ exports[`should render correctly 1`] = `
className="branch-analysis-time spacer-right"
>
<TimeFormatter
- date="2017-03-02"
+ date={2017-03-01T08:37:01.000Z}
long={false}
>
<Component />
@@ -277,3 +295,91 @@ exports[`should render correctly 1`] = `
</div>
</Fragment>
`;
+
+exports[`should render correctly: empty 1`] = `
+<Fragment>
+ <div
+ className="spacer-bottom"
+ >
+ baseline.analysis_from
+ <Select
+ autoBlur={true}
+ className="input-medium spacer-left"
+ clearable={false}
+ onChange={[MockFunction]}
+ options={
+ Array [
+ Object {
+ "label": "baseline.branch_analyses.ranges.30days",
+ "value": 30,
+ },
+ Object {
+ "label": "baseline.branch_analyses.ranges.allTime",
+ "value": 0,
+ },
+ ]
+ }
+ searchable={false}
+ value={30}
+ />
+ </div>
+ <div
+ className="branch-analysis-list-wrapper"
+ >
+ <div
+ className="bordered branch-analysis-list"
+ onScroll={[MockFunction]}
+ >
+ <div
+ className="big-spacer-top big-spacer-bottom strong"
+ >
+ baseline.no_analyses
+ </div>
+ </div>
+ </div>
+</Fragment>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<Fragment>
+ <div
+ className="spacer-bottom"
+ >
+ baseline.analysis_from
+ <Select
+ autoBlur={true}
+ className="input-medium spacer-left"
+ clearable={false}
+ onChange={[MockFunction]}
+ options={
+ Array [
+ Object {
+ "label": "baseline.branch_analyses.ranges.30days",
+ "value": 30,
+ },
+ Object {
+ "label": "baseline.branch_analyses.ranges.allTime",
+ "value": 0,
+ },
+ ]
+ }
+ searchable={false}
+ value={30}
+ />
+ </div>
+ <div
+ className="branch-analysis-list-wrapper"
+ >
+ <div
+ className="bordered branch-analysis-list"
+ onScroll={[MockFunction]}
+ >
+ <DeferredSpinner
+ className="big-spacer-top"
+ timeout={100}
+ />
+ <ul />
+ </div>
+ </div>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap
index e9ffc30753c..0b438ba102a 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly 1`] = `
+exports[`should render correctly: multiple branches 1`] = `
<Modal
contentLabel="baseline.new_code_period_for_branch_x.master"
onRequestClose={[Function]}
@@ -40,6 +40,108 @@ exports[`should render correctly 1`] = `
onSelect={[Function]}
selected={false}
/>
+ <BaselineSettingReferenceBranch
+ branchList={
+ Array [
+ Object {
+ "disabled": true,
+ "isMain": true,
+ "value": "master",
+ },
+ Object {
+ "disabled": false,
+ "isMain": false,
+ "value": "branch-6.7",
+ },
+ ]
+ }
+ onChangeReferenceBranch={[Function]}
+ onSelect={[Function]}
+ referenceBranch="branch-6.7"
+ selected={false}
+ settingLevel="branch"
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <ResetButtonLink
+ onClick={[MockFunction]}
+ >
+ cancel
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`should render correctly: only one branch 1`] = `
+<Modal
+ contentLabel="baseline.new_code_period_for_branch_x.master"
+ onRequestClose={[Function]}
+ size="large"
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ baseline.new_code_period_for_branch_x.master
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body modal-container branch-baseline-setting-modal"
+ >
+ <div
+ className="display-flex-row huge-spacer-bottom"
+ role="radiogroup"
+ >
+ <BaselineSettingPreviousVersion
+ isDefault={false}
+ onSelect={[Function]}
+ selected={false}
+ />
+ <BaselineSettingDays
+ days="30"
+ isChanged={false}
+ isValid={false}
+ onChangeDays={[Function]}
+ onSelect={[Function]}
+ selected={false}
+ />
+ <BaselineSettingAnalysis
+ onSelect={[Function]}
+ selected={false}
+ />
+ <BaselineSettingReferenceBranch
+ branchList={
+ Array [
+ Object {
+ "disabled": true,
+ "isMain": true,
+ "value": "master",
+ },
+ ]
+ }
+ onChangeReferenceBranch={[Function]}
+ onSelect={[Function]}
+ referenceBranch=""
+ selected={false}
+ settingLevel="branch"
+ />
</div>
</div>
<footer
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchList-test.tsx.snap
index 5c2965d1178..863633dce9e 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchList-test.tsx.snap
@@ -23,129 +23,86 @@ exports[`should render correctly 1`] = `
</tr>
</thead>
<tbody>
- <tr
+ <BranchListRow
+ branch={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": true,
+ "name": "master",
+ "newCodePeriod": Object {
+ "effectiveValue": undefined,
+ "type": "NUMBER_OF_DAYS",
+ "value": "27",
+ },
+ }
+ }
+ existingBranches={
+ Array [
+ "master",
+ "branch-6.7",
+ "branch-7.0",
+ ]
+ }
+ inheritedSetting={
+ Object {
+ "type": "PREVIOUS_VERSION",
+ }
+ }
key="master"
- >
- <td
- className="nowrap"
- >
- <BranchLikeIcon
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- "newCodePeriod": Object {
- "effectiveValue": undefined,
- "type": "NUMBER_OF_DAYS",
- "value": "27",
- },
- }
- }
- className="little-spacer-right"
- />
- master
- <div
- className="badge spacer-left"
- >
- branches.main_branch
- </div>
- </td>
- <td
- className="huge-spacer-right nowrap"
- >
- baseline.number_days: 27
- </td>
- <td
- className="text-right"
- >
- <ActionsDropdown>
- <ActionsDropdownItem
- onClick={[Function]}
- >
- edit
- </ActionsDropdownItem>
- <ActionsDropdownItem
- onClick={[Function]}
- >
- reset_to_default
- </ActionsDropdownItem>
- </ActionsDropdown>
- </td>
- </tr>
- <tr
+ onOpenEditModal={[Function]}
+ onResetToDefault={[Function]}
+ />
+ <BranchListRow
+ branch={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": false,
+ "name": "branch-6.7",
+ }
+ }
+ existingBranches={
+ Array [
+ "master",
+ "branch-6.7",
+ "branch-7.0",
+ ]
+ }
+ inheritedSetting={
+ Object {
+ "type": "PREVIOUS_VERSION",
+ }
+ }
key="branch-6.7"
- >
- <td
- className="nowrap"
- >
- <BranchLikeIcon
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": false,
- "name": "branch-6.7",
- }
- }
- className="little-spacer-right"
- />
- branch-6.7
- </td>
- <td
- className="huge-spacer-right nowrap"
- >
- branch_list.default_setting
- </td>
- <td
- className="text-right"
- >
- <ActionsDropdown>
- <ActionsDropdownItem
- onClick={[Function]}
- >
- edit
- </ActionsDropdownItem>
- </ActionsDropdown>
- </td>
- </tr>
- <tr
+ onOpenEditModal={[Function]}
+ onResetToDefault={[Function]}
+ />
+ <BranchListRow
+ branch={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": false,
+ "name": "branch-7.0",
+ }
+ }
+ existingBranches={
+ Array [
+ "master",
+ "branch-6.7",
+ "branch-7.0",
+ ]
+ }
+ inheritedSetting={
+ Object {
+ "type": "PREVIOUS_VERSION",
+ }
+ }
key="branch-7.0"
- >
- <td
- className="nowrap"
- >
- <BranchLikeIcon
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": false,
- "name": "branch-7.0",
- }
- }
- className="little-spacer-right"
- />
- branch-7.0
- </td>
- <td
- className="huge-spacer-right nowrap"
- >
- branch_list.default_setting
- </td>
- <td
- className="text-right"
- >
- <ActionsDropdown>
- <ActionsDropdownItem
- onClick={[Function]}
- >
- edit
- </ActionsDropdownItem>
- </ActionsDropdown>
- </td>
- </tr>
+ onOpenEditModal={[Function]}
+ onResetToDefault={[Function]}
+ />
</tbody>
</table>
</Fragment>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchListRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchListRow-test.tsx.snap
new file mode 100644
index 00000000000..abb087dc5f7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchListRow-test.tsx.snap
@@ -0,0 +1,308 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: branch with number of days 1`] = `
+<tr
+ className=""
+>
+ <td
+ className="nowrap"
+ >
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": false,
+ "name": "branch-6.7",
+ "newCodePeriod": Object {
+ "type": "NUMBER_OF_DAYS",
+ "value": "21",
+ },
+ }
+ }
+ className="little-spacer-right"
+ />
+ branch-6.7
+ </td>
+ <td
+ className="huge-spacer-right nowrap"
+ >
+ <Tooltip>
+ <span>
+ baseline.number_days: 21
+ </span>
+ </Tooltip>
+ </td>
+ <td
+ className="text-right"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ edit
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ reset_to_default
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
+
+exports[`should render correctly: branch with previous version 1`] = `
+<tr
+ className=""
+>
+ <td
+ className="nowrap"
+ >
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": false,
+ "name": "branch-6.7",
+ "newCodePeriod": Object {
+ "type": "PREVIOUS_VERSION",
+ },
+ }
+ }
+ className="little-spacer-right"
+ />
+ branch-6.7
+ </td>
+ <td
+ className="huge-spacer-right nowrap"
+ >
+ <Tooltip>
+ <span>
+ baseline.previous_version
+ </span>
+ </Tooltip>
+ </td>
+ <td
+ className="text-right"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ edit
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ reset_to_default
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
+
+exports[`should render correctly: branch with reference branch 1`] = `
+<tr
+ className=""
+>
+ <td
+ className="nowrap"
+ >
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": false,
+ "name": "branch-6.7",
+ "newCodePeriod": Object {
+ "type": "REFERENCE_BRANCH",
+ "value": "master",
+ },
+ }
+ }
+ className="little-spacer-right"
+ />
+ branch-6.7
+ </td>
+ <td
+ className="huge-spacer-right nowrap"
+ >
+ <Tooltip>
+ <span>
+ baseline.reference_branch: master
+ </span>
+ </Tooltip>
+ </td>
+ <td
+ className="text-right"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ edit
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ reset_to_default
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
+
+exports[`should render correctly: branch with specific analysis 1`] = `
+<tr
+ className=""
+>
+ <td
+ className="nowrap"
+ >
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": false,
+ "name": "branch-6.7",
+ "newCodePeriod": Object {
+ "effectiveValue": "2018-12-02T13:01:12",
+ "type": "SPECIFIC_ANALYSIS",
+ "value": "A85835",
+ },
+ }
+ }
+ className="little-spacer-right"
+ />
+ branch-6.7
+ </td>
+ <td
+ className="huge-spacer-right nowrap"
+ >
+ <Tooltip>
+ <span>
+ baseline.specific_analysis:
+ <DateTimeFormatter
+ date="2018-12-02T13:01:12"
+ />
+ </span>
+ </Tooltip>
+ </td>
+ <td
+ className="text-right"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ edit
+ </ActionsDropdownItem>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ reset_to_default
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
+
+exports[`should render correctly: faulty branch 1`] = `
+<tr
+ className="branch-setting-warning"
+>
+ <td
+ className="nowrap"
+ >
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": false,
+ "name": "branch-7.3",
+ }
+ }
+ className="little-spacer-right"
+ />
+ branch-7.3
+ </td>
+ <td
+ className="huge-spacer-right nowrap"
+ >
+ <Tooltip
+ overlay="baseline.reference_branch.invalid_branch_setting.branch-7.3"
+ >
+ <span>
+ <WarningIcon
+ className="little-spacer-right"
+ />
+ branch_list.default_setting
+ </span>
+ </Tooltip>
+ </td>
+ <td
+ className="text-right"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ edit
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
+
+exports[`should render correctly: main branch with default 1`] = `
+<tr
+ className=""
+>
+ <td
+ className="nowrap"
+ >
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ className="little-spacer-right"
+ />
+ master
+ <div
+ className="badge spacer-left"
+ >
+ branches.main_branch
+ </div>
+ </td>
+ <td
+ className="huge-spacer-right nowrap"
+ >
+ <Tooltip>
+ <span>
+ branch_list.default_setting
+ </span>
+ </Tooltip>
+ </td>
+ <td
+ className="text-right"
+ >
+ <ActionsDropdown>
+ <ActionsDropdownItem
+ onClick={[Function]}
+ >
+ edit
+ </ActionsDropdownItem>
+ </ActionsDropdown>
+ </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
index 67c95df1550..a6c9acaad35 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
@@ -40,7 +40,7 @@ exports[`should render correctly 1`] = `
</Radio>
</div>
<div
- className="big-spacer-left big-spacer-right branch-baseline-setting-modal"
+ className="big-spacer-left big-spacer-right project-baseline-setting"
>
<div
className="display-flex-row big-spacer-bottom"
@@ -60,16 +60,33 @@ exports[`should render correctly 1`] = `
onSelect={[MockFunction]}
selected={false}
/>
+ <BaselineSettingReferenceBranch
+ branchList={
+ Array [
+ Object {
+ "isMain": true,
+ "value": "master",
+ },
+ ]
+ }
+ disabled={true}
+ onChangeReferenceBranch={[MockFunction]}
+ onSelect={[MockFunction]}
+ referenceBranch="master"
+ selected={false}
+ settingLevel="project"
+ />
</div>
</div>
<div
className="big-spacer-top invisible"
>
- <p
+ <Alert
className="spacer-bottom"
+ variant="info"
>
baseline.next_analysis_notice
- </p>
+ </Alert>
<DeferredSpinner
className="spacer-right"
loading={false}
@@ -130,7 +147,7 @@ exports[`should render correctly 2`] = `
</Radio>
</div>
<div
- className="big-spacer-left big-spacer-right branch-baseline-setting-modal"
+ className="big-spacer-left big-spacer-right project-baseline-setting"
>
<div
className="display-flex-row big-spacer-bottom"
@@ -160,11 +177,12 @@ exports[`should render correctly 2`] = `
<div
className="big-spacer-top invisible"
>
- <p
+ <Alert
className="spacer-bottom"
+ variant="info"
>
baseline.next_analysis_notice
- </p>
+ </Alert>
<DeferredSpinner
className="spacer-right"
loading={false}
@@ -225,7 +243,7 @@ exports[`should render correctly 3`] = `
</Radio>
</div>
<div
- className="big-spacer-left big-spacer-right branch-baseline-setting-modal"
+ className="big-spacer-left big-spacer-right project-baseline-setting"
>
<div
className="display-flex-row big-spacer-bottom"
@@ -255,11 +273,12 @@ exports[`should render correctly 3`] = `
<div
className="big-spacer-top invisible"
>
- <p
+ <Alert
className="spacer-bottom"
+ variant="info"
>
baseline.next_analysis_notice
- </p>
+ </Alert>
<DeferredSpinner
className="spacer-right"
loading={false}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts
index afc2ea87ef9..665fe6bb386 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts
@@ -20,22 +20,26 @@
import { getSettingValue, validateSetting } from '../../utils';
describe('getSettingValue', () => {
+ const state = {
+ analysis: 'analysis',
+ days: '35',
+ referenceBranch: 'branch-4.2'
+ };
+
it('should work for Days', () => {
- expect(getSettingValue({ analysis: 'analysis', days: '35', type: 'NUMBER_OF_DAYS' })).toBe(
- '35'
- );
+ expect(getSettingValue({ ...state, type: 'NUMBER_OF_DAYS' })).toBe(state.days);
});
it('should work for Analysis', () => {
- expect(getSettingValue({ analysis: 'analysis1', days: '35', type: 'SPECIFIC_ANALYSIS' })).toBe(
- 'analysis1'
- );
+ expect(getSettingValue({ ...state, type: 'SPECIFIC_ANALYSIS' })).toBe(state.analysis);
});
it('should work for Previous version', () => {
- expect(
- getSettingValue({ analysis: 'analysis1', days: '35', type: 'PREVIOUS_VERSION' })
- ).toBeUndefined();
+ expect(getSettingValue({ ...state, type: 'PREVIOUS_VERSION' })).toBeUndefined();
+ });
+
+ it('should work for Reference branch', () => {
+ expect(getSettingValue({ ...state, type: 'REFERENCE_BRANCH' })).toBe(state.referenceBranch);
});
});
@@ -90,6 +94,24 @@ describe('validateSettings', () => {
selected: 'SPECIFIC_ANALYSIS'
})
).toEqual({ isChanged: true, isValid: true });
+ expect(
+ validateSetting({
+ currentSetting: 'REFERENCE_BRANCH',
+ currentSettingValue: 'master',
+ days: '',
+ referenceBranch: 'master',
+ selected: 'REFERENCE_BRANCH'
+ })
+ ).toEqual({ isChanged: false, isValid: true });
+ expect(
+ validateSetting({
+ currentSetting: 'REFERENCE_BRANCH',
+ currentSettingValue: 'master',
+ days: '',
+ referenceBranch: '',
+ selected: 'REFERENCE_BRANCH'
+ })
+ ).toEqual({ isChanged: true, isValid: false });
});
it('should validate at project level', () => {
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/styles.css b/server/sonar-web/src/main/js/apps/projectBaseline/styles.css
index 0d7889c79a6..26cdb745786 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/styles.css
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/styles.css
@@ -21,7 +21,9 @@
padding: calc(4 * var(--gridSize));
}
-.project-baseline-selector > .branch-baseline-setting-modal {
+.project-baseline-setting {
+ display: flex;
+ flex-direction: column;
max-height: 60vh;
padding-top: 2px;
}
@@ -35,6 +37,7 @@
}
.branch-baseline-setting-modal {
+ min-height: 450px;
display: flex;
flex-direction: column;
}
@@ -125,6 +128,10 @@
text-overflow: ellipsis;
}
+.branch-setting-warning {
+ background-color: var(--alertBackgroundWarning) !important;
+}
+
.project-activity-event-icon.VERSION {
color: var(--blue);
}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
index 7a5254a4326..296c79d7aae 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
@@ -26,15 +26,19 @@ export function validateDays(days: string) {
export function getSettingValue({
analysis,
days,
+ referenceBranch,
type
}: {
analysis?: string;
days?: string;
+ referenceBranch?: string;
type?: T.NewCodePeriodSettingType;
}) {
switch (type) {
case 'NUMBER_OF_DAYS':
return days;
+ case 'REFERENCE_BRANCH':
+ return referenceBranch;
case 'SPECIFIC_ANALYSIS':
return analysis;
default:
@@ -47,16 +51,18 @@ export function validateSetting(state: {
currentSetting?: T.NewCodePeriodSettingType;
currentSettingValue?: string;
days: string;
- selected?: T.NewCodePeriodSettingType;
overrideGeneralSetting?: boolean;
+ referenceBranch?: string;
+ selected?: T.NewCodePeriodSettingType;
}) {
const {
analysis = '',
currentSetting,
currentSettingValue,
days,
- selected,
- overrideGeneralSetting
+ overrideGeneralSetting,
+ referenceBranch = '',
+ selected
} = state;
let isChanged;
@@ -67,14 +73,16 @@ export function validateSetting(state: {
overrideGeneralSetting === false ||
selected !== currentSetting ||
(selected === 'NUMBER_OF_DAYS' && days !== currentSettingValue) ||
- (selected === 'SPECIFIC_ANALYSIS' && analysis !== currentSettingValue);
+ (selected === 'SPECIFIC_ANALYSIS' && analysis !== currentSettingValue) ||
+ (selected === 'REFERENCE_BRANCH' && referenceBranch !== currentSettingValue);
}
const isValid =
overrideGeneralSetting === false ||
selected === 'PREVIOUS_VERSION' ||
(selected === 'SPECIFIC_ANALYSIS' && analysis.length > 0) ||
- (selected === 'NUMBER_OF_DAYS' && validateDays(days));
+ (selected === 'NUMBER_OF_DAYS' && validateDays(days)) ||
+ (selected === 'REFERENCE_BRANCH' && referenceBranch.length > 0);
return { isChanged, isValid };
}
diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts
index a04d7a4c7c4..df78c685b06 100644
--- a/server/sonar-web/src/main/js/types/types.d.ts
+++ b/server/sonar-web/src/main/js/types/types.d.ts
@@ -503,7 +503,8 @@ declare namespace T {
export type NewCodePeriodSettingType =
| 'PREVIOUS_VERSION'
| 'NUMBER_OF_DAYS'
- | 'SPECIFIC_ANALYSIS';
+ | 'SPECIFIC_ANALYSIS'
+ | 'REFERENCE_BRANCH';
export interface Notification {
channel: string;
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index 1725ed051e1..9a62a3587d8 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -10522,10 +10522,10 @@ sockjs@0.3.19:
faye-websocket "^0.10.0"
uuid "^3.0.1"
-sonar-ui-common@1.0.4:
- version "1.0.4"
- resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-1.0.4.tgz#342cb674a560a79cbae47ecbf60ab47fe1052fa4"
- integrity sha1-NCy2dKVgp5y65H7L9gq0f+EFL6Q=
+sonar-ui-common@1.0.6:
+ version "1.0.6"
+ resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-1.0.6.tgz#0b22b9b35e4e210b34304780328b9831bf1f7a58"
+ integrity sha1-CyK5s15OIQs0MEeAMouYMb8felg=
dependencies:
"@types/react-select" "1.2.6"
classnames "2.2.6"
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 7affd182602..3cee83507fc 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -563,8 +563,8 @@ project_branch_pull_request.table.branch=Branch
project_branch_pull_request.table.pull_request=Pull Request
project_branch_pull_request.last_analysis_date=Last Analysis Date
-project_baseline.page=New Code Period
-project_baseline.page.description=Use this page to manage the New Code Period of your project. {link}
+project_baseline.page=New Code
+project_baseline.page.description=Use this page to define the New Code of your project. {link}
project_baseline.page.description.link=Learn More
project_baseline.page.description2=You can adjust this setting globally in {link}
project_baseline.page.description2.link=General Settings
@@ -574,18 +574,26 @@ project_baseline.specific_setting=Define a specific setting for this project
project_baseline.configure_branches=Set a specific setting for a branch
baseline.previous_version=Previous version
-baseline.previous_version.description=The New Code Period will begin with the analysis following the previous version.
+baseline.previous_version.description=The New Code will be based on the analysis following the previous version.
baseline.number_days=Number of days
-baseline.number_days.description=A floating New Code Period window set to a specific number of days.
-baseline.specific_date=Specific date
-baseline.specific_date.description=Set a specific date as the start of the New Code Period. (First analysis on this date will be used)
+baseline.number_days.description=A floating window set to a specific number of days used to define New Code.
baseline.specific_analysis=Specific analysis
-baseline.specific_analysis.description=Choose an analysis as the baseline for the New Code Period.
+baseline.specific_analysis.description=Choose an analysis as the baseline for the New Code.
+baseline.reference_branch=Reference branch
+
+baseline.reference_branch.description=Choose a branch as the reference for the New Code.
+baseline.reference_branch.description2=The branch you select as the reference branch will need its own New Code definition to prevent it from using itself as a reference.
baseline.specify_days=Specify a number of days
baseline.last_analysis_before=Last analysis before
baseline.next_analysis_notice=Changes will take effect after the next analysis
+baseline.reference_branch.choose=Choose a branch
+baseline.reference_branch.does_not_exist=Branch {0} could not be found in SonarQube.
+baseline.reference_branch.cannot_be_itself=A branch cannot be used as its own reference branch
+baseline.reference_branch.invalid_branch_setting=Branch {0} cannot use itself as a reference. Define a specific setting instead of using the project-level setting.
+baseline.edit_branch_setting=Edit the branch's setting
+
branch_list.branch=Branch
branch_list.current_setting=Setting
branch_list.current_baseline=Current Baseline
@@ -1000,10 +1008,10 @@ settings.analysis_scope.wildcards.zero_more_char=Match zero or more characters
settings.analysis_scope.wildcards.zero_more_dir=Match zero or more directories
settings.analysis_scope.wildcards.single_char=Match a single character
-settings.new_code_period.category=New Code Period
-settings.new_code_period.title=Default New Code Period behavior
-settings.new_code_period.description=The New Code Period is the period used to compare measures and track new issues. {link}
-settings.new_code_period.description2=This setting is the default for all projects. A specific New Code Period setting can be configured at project level.
+settings.new_code_period.category=New Code
+settings.new_code_period.title=Default New Code behavior
+settings.new_code_period.description=The New Code definition is used to compare measures and track new issues. {link}
+settings.new_code_period.description2=This setting is the default for all projects. A specific New Code definition can be configured at project level.
settings.languages.select_a_language_placeholder=Select a language