Browse Source

SONAR-13391 Add reference branch option to new code setting

tags/8.4.0.35506
Jeremy Davis 4 years ago
parent
commit
b7d595d1a4
36 changed files with 2087 additions and 570 deletions
  1. 1
    1
      server/sonar-docs/package.json
  2. 4
    4
      server/sonar-docs/yarn.lock
  3. 1
    1
      server/sonar-web/package.json
  4. 27
    29
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
  5. 45
    47
      server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
  6. 64
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx
  7. 113
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
  8. 25
    143
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
  9. 186
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx
  10. 29
    5
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
  11. 16
    60
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
  12. 135
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
  13. 32
    7
      server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
  14. 12
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
  15. 31
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/AppHeader-test.tsx
  16. 117
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx
  17. 26
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisList-test.tsx
  18. 84
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisListRenderer-test.tsx
  19. 6
    2
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx
  20. 14
    44
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx
  21. 103
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx
  22. 4
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
  23. 3
    44
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
  24. 80
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap
  25. 210
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap
  26. 126
    20
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisListRenderer-test.tsx.snap
  27. 103
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap
  28. 77
    120
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchList-test.tsx.snap
  29. 308
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchListRow-test.tsx.snap
  30. 28
    9
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
  31. 31
    9
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts
  32. 8
    1
      server/sonar-web/src/main/js/apps/projectBaseline/styles.css
  33. 13
    5
      server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
  34. 2
    1
      server/sonar-web/src/main/js/types/types.d.ts
  35. 4
    4
      server/sonar-web/yarn.lock
  36. 19
    11
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-docs/package.json View File

@@ -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": {

+ 4
- 4
server/sonar-docs/yarn.lock View File

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

+ 1
- 1
server/sonar-web/package.json View File

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

+ 27
- 29
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx View File

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

+ 45
- 47
server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx View File

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

+ 64
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/AppHeader.tsx View File

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

+ 113
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx View File

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

+ 25
- 143
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx View File

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

+ 186
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisListRenderer.tsx View File

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

+ 29
- 5
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx View File

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

+ 16
- 60
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx View File

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

+ 135
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx View File

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

+ 32
- 7
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx View File

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

+ 12
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx View File

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

+ 31
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/AppHeader-test.tsx View File

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

+ 117
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx View File

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

+ 26
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisList-test.tsx View File

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

+ 84
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisListRenderer-test.tsx View File

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

+ 6
- 2
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx View File

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

+ 14
- 44
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx View File

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

+ 103
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx View File

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

+ 4
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx View File

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

+ 3
- 44
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap View File

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

+ 80
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/AppHeader-test.tsx.snap View File

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

+ 210
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap View File

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

server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisList-test.tsx.snap → server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchAnalysisListRenderer-test.tsx.snap View File

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

+ 103
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap View File

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

+ 77
- 120
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchList-test.tsx.snap View File

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

+ 308
- 0
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchListRow-test.tsx.snap View File

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

+ 28
- 9
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap View File

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

+ 31
- 9
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts View File

@@ -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', () => {

+ 8
- 1
server/sonar-web/src/main/js/apps/projectBaseline/styles.css View File

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

+ 13
- 5
server/sonar-web/src/main/js/apps/projectBaseline/utils.ts View File

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

+ 2
- 1
server/sonar-web/src/main/js/types/types.d.ts View File

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

+ 4
- 4
server/sonar-web/yarn.lock View File

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

+ 19
- 11
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

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


Loading…
Cancel
Save