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