diff options
24 files changed, 277 insertions, 99 deletions
diff --git a/server/sonar-web/src/main/js/api/newCodePeriod.ts b/server/sonar-web/src/main/js/api/newCodePeriod.ts index 8043702a12b..1c21cefa735 100644 --- a/server/sonar-web/src/main/js/api/newCodePeriod.ts +++ b/server/sonar-web/src/main/js/api/newCodePeriod.ts @@ -31,7 +31,7 @@ export function setNewCodePeriod(data: { project?: string; branch?: string; type: T.NewCodePeriodSettingType; - value: string | null; + value?: string; }): Promise<void> { return post('/api/new_code_periods/set', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts index 23a911a076b..c5ccff569da 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -115,10 +115,7 @@ declare namespace T { export type BranchType = 'LONG' | 'SHORT'; export interface BranchWithNewCodePeriod extends Branch { - newCodePeriod?: { - type: NewCodePeriodSettingType; - value: string | null; - }; + newCodePeriod?: NewCodePeriod; } export interface Breadcrumb { @@ -506,12 +503,19 @@ declare namespace T { qualityGate?: string; } + export interface NewCodePeriod { + type?: NewCodePeriodSettingType; + value?: string; + effectiveValue?: string; + } + export interface NewCodePeriodBranch { projectKey: string; branchKey: string; inherited?: boolean; type?: NewCodePeriodSettingType; - value?: string | null; + value?: string; + effectiveValue?: string; } export type NewCodePeriodSettingType = diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx index 19a8c041be4..d6de9e60612 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as classNames from 'classnames'; +import { isEqual } from 'date-fns'; import { throttle } from 'lodash'; import * as React from 'react'; import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; @@ -45,6 +46,7 @@ interface Props { deleteAnalysis: (analysis: string) => Promise<void>; deleteEvent: (analysis: string, event: string) => Promise<void>; initializing: boolean; + leakPeriodDate?: Date; project: { qualifier: string }; query: Query; updateQuery: (changes: Partial<Query>) => void; @@ -155,6 +157,40 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro this.props.updateQuery({ selectedDate: date }); }; + shouldRenderBaselineMarker(analysis: ParsedAnalysis): boolean { + return Boolean( + analysis.manualNewCodePeriodBaseline || + (this.props.leakPeriodDate && isEqual(this.props.leakPeriodDate, analysis.date)) + ); + } + + renderAnalysis(analysis: ParsedAnalysis) { + const firstAnalysisKey = this.props.analyses[0].key; + + const selectedDate = this.props.query.selectedDate + ? this.props.query.selectedDate.valueOf() + : null; + + return ( + <ProjectActivityAnalysis + addCustomEvent={this.props.addCustomEvent} + addVersion={this.props.addVersion} + analysis={analysis} + canAdmin={this.props.canAdmin} + canCreateVersion={this.props.project.qualifier === 'TRK'} + canDeleteAnalyses={this.props.canDeleteAnalyses} + changeEvent={this.props.changeEvent} + deleteAnalysis={this.props.deleteAnalysis} + deleteEvent={this.props.deleteEvent} + isBaseline={this.shouldRenderBaselineMarker(analysis)} + isFirst={analysis.key === firstAnalysisKey} + key={analysis.key} + selected={analysis.date.valueOf() === selectedDate} + updateSelectedDate={this.updateSelectedDate} + /> + ); + } + render() { const byVersionByDay = getAnalysesByVersionByDay(this.props.analyses, this.props.query); const hasFilteredData = @@ -174,11 +210,6 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro ); } - const firstAnalysisKey = this.props.analyses[0].key; - const selectedDate = this.props.query.selectedDate - ? this.props.query.selectedDate.valueOf() - : null; - return ( <ul className={classNames('project-activity-versions-list', this.props.className)} @@ -212,23 +243,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro </div> <ul className="project-activity-analyses-list"> {version.byDay[day] != null && - version.byDay[day].map(analysis => ( - <ProjectActivityAnalysis - addCustomEvent={this.props.addCustomEvent} - addVersion={this.props.addVersion} - analysis={analysis} - canAdmin={this.props.canAdmin} - canCreateVersion={this.props.project.qualifier === 'TRK'} - canDeleteAnalyses={this.props.canDeleteAnalyses} - changeEvent={this.props.changeEvent} - deleteAnalysis={this.props.deleteAnalysis} - deleteEvent={this.props.deleteEvent} - isFirst={analysis.key === firstAnalysisKey} - key={analysis.key} - selected={analysis.date.valueOf() === selectedDate} - updateSelectedDate={this.updateSelectedDate} - /> - ))} + version.byDay[day].map(analysis => this.renderAnalysis(analysis))} </ul> </li> ))} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx index e24bd6e9f87..515388f74d8 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx @@ -23,6 +23,7 @@ import ActionsDropdown, { ActionsDropdownDivider, ActionsDropdownItem } from 'sonar-ui-common/components/controls/ActionsDropdown'; +import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; import { parseDate } from 'sonar-ui-common/helpers/dates'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; @@ -42,6 +43,7 @@ interface Props { changeEvent: (event: string, name: string) => Promise<void>; deleteAnalysis: (analysis: string) => Promise<void>; deleteEvent: (analysis: string, event: string) => Promise<void>; + isBaseline: boolean; isFirst: boolean; selected: boolean; updateSelectedDate: (date: Date) => void; @@ -51,7 +53,6 @@ interface State { addEventForm: boolean; addVersionForm: boolean; removeAnalysisForm: boolean; - suppressVersionTooltip?: boolean; } export default class ProjectActivityAnalysis extends React.PureComponent<Props, State> { @@ -102,22 +103,31 @@ export default class ProjectActivityAnalysis extends React.PureComponent<Props, this.setState({ addVersionForm: true }); }; - handleTimeTooltipHide = () => { - this.setState({ suppressVersionTooltip: false }); - }; - - handleTimeTooltipShow = () => { - this.setState({ suppressVersionTooltip: true }); - }; - closeAddVersionForm = () => { if (this.mounted) { this.setState({ addVersionForm: false }); } }; + renderBaselineMarker() { + return ( + <div className="baseline-marker"> + <div className="wedge" /> + <hr /> + <div className="label display-flex-center"> + {translate('project_activity.new_code_period_start')} + <HelpTooltip + className="little-spacer-left" + overlay={translate('project_activity.new_code_period_start.help')} + placement="top" + /> + </div> + </div> + ); + } + render() { - const { analysis, isFirst, canAdmin, canCreateVersion } = this.props; + const { analysis, isBaseline, isFirst, canAdmin, canCreateVersion } = this.props; const { date, events } = analysis; const parsedDate = parseDate(date); const hasVersion = events.find(event => event.category === 'VERSION') != null; @@ -222,6 +232,8 @@ export default class ProjectActivityAnalysis extends React.PureComponent<Props, isFirst={this.props.isFirst} /> )} + + {isBaseline && this.renderBaselineMarker()} </li> </Tooltip> ); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx index f5ad361bd9a..8766d537f91 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx @@ -82,6 +82,9 @@ export default function ProjectActivityApp(props: Props) { deleteAnalysis={props.deleteAnalysis} deleteEvent={props.deleteEvent} initializing={props.initializing} + leakPeriodDate={ + props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined + } project={props.project} query={props.query} updateQuery={props.updateQuery} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx index 3f4e5a879ed..4a40545a1e4 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx @@ -34,10 +34,12 @@ jest.mock('sonar-ui-common/helpers/dates', () => { return { ...actual, toShortNotSoISOString: (date: string) => 'ISO.' + date }; }); +const DATE = parseDate('2016-10-27T16:33:50+0000'); + const ANALYSES = [ { key: 'A1', - date: parseDate('2016-10-27T16:33:50+0000'), + date: DATE, events: [{ key: 'E1', category: 'VERSION', name: '6.5-SNAPSHOT' }] }, { key: 'A2', date: parseDate('2016-10-27T12:21:15+0000'), events: [] }, @@ -66,6 +68,7 @@ const DEFAULT_PROPS: ProjectActivityAnalysesList['props'] = { deleteAnalysis: jest.fn().mockResolvedValue(undefined), deleteEvent: jest.fn().mockResolvedValue(undefined), initializing: false, + leakPeriodDate: parseDate('2016-10-27T12:21:15+0000'), project: { qualifier: 'TRK' }, query: { category: '', @@ -91,8 +94,8 @@ it('should correctly filter analyses by date range', () => { wrapper.setProps({ query: { ...DEFAULT_PROPS.query, - from: parseDate('2016-10-27T16:33:50+0000'), - to: parseDate('2016-10-27T16:33:50+0000') + from: DATE, + to: DATE } }); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx index 8e842ab107c..175ea87e343 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx @@ -38,6 +38,7 @@ it('should render correctly', () => { expect( shallowRender({ analysis: mockParsedAnalysis({ buildString: '1.0.234' }) }) ).toMatchSnapshot(); + expect(shallowRender({ isBaseline: true })).toMatchSnapshot(); }); it('should show the correct admin options', () => { @@ -87,6 +88,7 @@ function shallowRender(props: Partial<ProjectActivityAnalysis['props']> = {}) { changeEvent={jest.fn()} deleteAnalysis={jest.fn()} deleteEvent={jest.fn()} + isBaseline={false} isFirst={false} selected={false} updateSelectedDate={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap index 801e98e8a31..37ab95a8e9e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap @@ -67,6 +67,7 @@ exports[`should correctly filter analyses by category 1`] = ` changeEvent={[MockFunction]} deleteAnalysis={[MockFunction]} deleteEvent={[MockFunction]} + isBaseline={false} isFirst={false} key="A4" selected={false} @@ -146,6 +147,7 @@ exports[`should correctly filter analyses by date range 1`] = ` changeEvent={[MockFunction]} deleteAnalysis={[MockFunction]} deleteEvent={[MockFunction]} + isBaseline={false} isFirst={true} key="A1" selected={false} @@ -225,6 +227,7 @@ exports[`should render correctly 1`] = ` changeEvent={[MockFunction]} deleteAnalysis={[MockFunction]} deleteEvent={[MockFunction]} + isBaseline={false} isFirst={true} key="A1" selected={false} @@ -245,6 +248,7 @@ exports[`should render correctly 1`] = ` changeEvent={[MockFunction]} deleteAnalysis={[MockFunction]} deleteEvent={[MockFunction]} + isBaseline={true} isFirst={false} key="A2" selected={false} @@ -316,6 +320,7 @@ exports[`should render correctly 1`] = ` changeEvent={[MockFunction]} deleteAnalysis={[MockFunction]} deleteEvent={[MockFunction]} + isBaseline={false} isFirst={false} key="A3" selected={false} @@ -360,6 +365,7 @@ exports[`should render correctly 1`] = ` changeEvent={[MockFunction]} deleteAnalysis={[MockFunction]} deleteEvent={[MockFunction]} + isBaseline={false} isFirst={false} key="A4" selected={false} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap index 722f614714e..6e8e9a74b2a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap @@ -221,6 +221,65 @@ exports[`should render correctly 3`] = ` </Tooltip> `; +exports[`should render correctly 4`] = ` +<Tooltip + mouseEnterDelay={0.5} + overlay={ + <TimeFormatter + date={ + Object { + "toISOString": [Function], + "valueOf": [Function], + } + } + long={true} + /> + } + placement="left" +> + <li + className="project-activity-analysis" + data-date={1546333200000} + onClick={[Function]} + tabIndex={0} + > + <div + className="project-activity-time spacer-right" + > + <TimeFormatter + date={ + Object { + "toISOString": [Function], + "valueOf": [Function], + } + } + long={false} + > + <Component /> + </TimeFormatter> + </div> + <div + className="baseline-marker" + > + <div + className="wedge" + /> + <hr /> + <div + className="label display-flex-center" + > + project_activity.new_code_period_start + <HelpTooltip + className="little-spacer-left" + overlay="project_activity.new_code_period_start.help" + placement="top" + /> + </div> + </div> + </li> +</Tooltip> +`; + exports[`should show the correct admin options 1`] = ` <Tooltip mouseEnterDelay={0.5} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap index 324678b7dc9..635b6c84a4c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap @@ -80,6 +80,7 @@ exports[`should render correctly 1`] = ` deleteAnalysis={[MockFunction]} deleteEvent={[MockFunction]} initializing={false} + leakPeriodDate={2017-05-16T11:50:02.000Z} project={ Object { "key": "foo", diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css index acb32386265..b20002bd654 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css @@ -61,9 +61,6 @@ flex-shrink: 0; } -.project-activity-days-list { -} - .project-activity-day { margin-top: 8px; margin-bottom: 24px; @@ -79,14 +76,11 @@ font-weight: bold; } -.project-activity-analyses-list { -} - .project-activity-analysis { position: relative; display: flex; min-height: var(--smallControlHeight); - padding: calc(0.5 * var(--gridSize)); + padding: var(--gridSize) calc(0.5 * var(--gridSize)); border-top: 1px solid var(--barBorderColor); border-bottom: 1px solid var(--barBorderColor); cursor: pointer; @@ -294,3 +288,33 @@ .project-activity-graph-tooltip-value { font-weight: bold; } + +.baseline-marker { + position: absolute; + top: -10px; + left: 0; + right: 0; + display: flex; + flex-direction: row; + align-items: center; +} + +.baseline-marker > .wedge { + border: 10px solid transparent; + border-left-color: var(--leakBorderColor); +} + +.baseline-marker > hr { + border: none; + margin: 0 0 0 -11px; + background-color: var(--leakBorderColor); + height: 2px; + flex: 1 0 auto; +} + +.baseline-marker > .label { + background-color: var(--leakColor); + border: 1px solid var(--leakBorderColor); + padding: 2px 8px; + font-size: var(--verySmallFontSize); +} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchBaselineSettingModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchBaselineSettingModal-test.tsx index a30c435de00..230a2ec74c9 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchBaselineSettingModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchBaselineSettingModal-test.tsx @@ -104,7 +104,7 @@ describe('getSettingValue', () => { it('should work for Previous version', () => { wrapper.setState({ selected: 'PREVIOUS_VERSION' }); - expect(wrapper.instance().getSettingValue()).toBeNull(); + expect(wrapper.instance().getSettingValue()).toBeUndefined(); }); }); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchList-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchList-test.tsx index 73cff093697..0c34ed35b6b 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchList-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchList-test.tsx @@ -102,16 +102,32 @@ it('should render the right setting label', () => { 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: 'PREVIOUS_VERSION', value: null }) - ).toBe('baseline.previous_version'); - expect( - wrapper.instance().renderNewCodePeriodSetting({ type: 'SPECIFIC_ANALYSIS', value: 'A85835' }) - ).toBe('baseline.specific_analysis: A85835'); + 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={[]} component={mockComponent()} {...props} /> + <BranchList + branchLikes={[]} + component={mockComponent()} + inheritedSetting={{ type: 'PREVIOUS_VERSION' }} + {...props} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap index 51582805875..571628b3fda 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap @@ -11,6 +11,11 @@ exports[`should render correctly 1`] = ` branch_list.branch </th> <th + className="thin" + > + + </th> + <th className="thin nowrap huge-spacer-right" > branch_list.current_setting @@ -36,6 +41,7 @@ exports[`should render correctly 1`] = ` "isMain": true, "name": "master", "newCodePeriod": Object { + "effectiveValue": undefined, "type": "NUMBER_OF_DAYS", "value": "27", }, @@ -50,6 +56,7 @@ exports[`should render correctly 1`] = ` branches.main_branch </div> </td> + <td /> <td className="huge-spacer-right nowrap" > @@ -91,9 +98,7 @@ exports[`should render correctly 1`] = ` /> branch-6.7 </td> - <td - className="huge-spacer-right nowrap" - > + <td> <span className="badge badge-info" > @@ -101,6 +106,11 @@ exports[`should render correctly 1`] = ` </span> </td> <td + className="huge-spacer-right nowrap" + > + baseline.previous_version + </td> + <td className="text-right" > <ActionsDropdown> 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 992ffd8fc20..c88e2fc9e64 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 @@ -36,9 +36,9 @@ interface Props { interface State { currentSetting?: T.NewCodePeriodSettingType; - currentSettingValue?: string | number; + currentSettingValue?: string; days: string; - generalSetting?: { type: T.NewCodePeriodSettingType; value?: string }; + generalSetting?: T.NewCodePeriod; loading: boolean; saving: boolean; selected?: T.NewCodePeriodSettingType; @@ -126,7 +126,7 @@ export default class App extends React.PureComponent<Props, State> { const { days, selected } = this.state; const type = selected; - const value = type === 'NUMBER_OF_DAYS' ? days : null; + const value = type === 'NUMBER_OF_DAYS' ? days : undefined; if (type) { this.setState({ saving: true }); @@ -184,7 +184,7 @@ export default class App extends React.PureComponent<Props, State> { ); } - renderGeneralSetting(generalSetting: { type: T.NewCodePeriodSettingType; value?: string }) { + renderGeneralSetting(generalSetting: T.NewCodePeriod) { if (generalSetting.type === 'NUMBER_OF_DAYS') { return `${translate('baseline.number_days')} (${translateWithParameters( 'duration.days', @@ -238,15 +238,26 @@ export default class App extends React.PureComponent<Props, State> { currentSetting={currentSetting} currentSettingValue={currentSettingValue} days={days} - generalSetting={generalSetting} onSelectDays={this.handleSelectDays} onSelectSetting={this.handleSelectSetting} onSubmit={this.handleSubmit} saving={saving} selected={selected} /> - - <BranchList branchLikes={this.props.branchLikes} component={this.props.component} /> + {generalSetting && ( + <BranchList + branchLikes={this.props.branchLikes} + component={this.props.component} + inheritedSetting={ + currentSetting + ? { + type: currentSetting, + value: currentSettingValue + } + : generalSetting + } + /> + )} </div> )} </div> 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 c3803cacf14..4791f62ba6e 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 @@ -36,7 +36,7 @@ interface Props { analysis: string; branch: string; component: string; - onSelectAnalysis: (analysis: string) => void; + onSelectAnalysis: (analysis: ParsedAnalysis) => void; } interface State { @@ -216,7 +216,7 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State })} data-date={parseDate(analysis.date).valueOf()} key={analysis.key} - onClick={() => this.props.onSelectAnalysis(analysis.key)}> + onClick={() => this.props.onSelectAnalysis(analysis)}> <div className="branch-analysis-time spacer-right"> <TimeFormatter date={parseDate(analysis.date)} long={false}> {formattedTime => ( 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 095595ed2ff..b8dd08c6b66 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 @@ -21,8 +21,10 @@ import * as React from 'react'; import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; import Modal from 'sonar-ui-common/components/controls/Modal'; 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 { ParsedAnalysis } from '../../projectActivity/utils'; import { validateDays } from '../utils'; import BaselineSettingAnalysis from './BaselineSettingAnalysis'; import BaselineSettingDays from './BaselineSettingDays'; @@ -32,14 +34,12 @@ import BranchAnalysisList from './BranchAnalysisList'; interface Props { branch: T.BranchWithNewCodePeriod; component: string; - onClose: ( - branch?: string, - newSetting?: { type: T.NewCodePeriodSettingType; value: string | null } - ) => void; + onClose: (branch?: string, newSetting?: T.NewCodePeriod) => void; } interface State { analysis: string; + analysisDate?: Date; days: string; saving: boolean; selected?: T.NewCodePeriodSettingType; @@ -80,7 +80,7 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop case 'SPECIFIC_ANALYSIS': return this.state.analysis; default: - return null; + return undefined; } } @@ -88,7 +88,7 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop e.preventDefault(); const { branch, component } = this.props; - const { selected: type } = this.state; + const { analysisDate, selected: type } = this.state; const value = this.getSettingValue(); @@ -104,7 +104,11 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop this.setState({ saving: false }); - this.props.onClose(branch.name, { type, value }); + this.props.onClose(branch.name, { + type, + value, + effectiveValue: analysisDate && toNotSoISOString(analysisDate) + }); }, () => { this.setState({ @@ -117,7 +121,8 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop requestClose = () => this.props.onClose(); - handleSelectAnalysis = (analysis: string) => this.setState({ analysis }); + handleSelectAnalysis = (analysis: ParsedAnalysis) => + this.setState({ analysis: analysis.key, analysisDate: analysis.date }); handleSelectDays = (days: string) => this.setState({ days }); 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 691247fac7d..234cac81b58 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 @@ -25,12 +25,14 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { listBranchesNewCodePeriod, resetNewCodePeriod } from '../../../api/newCodePeriod'; import BranchIcon from '../../../components/icons-components/BranchIcon'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import { isLongLivingBranch, isMainBranch, sortBranchesAsTree } from '../../../helpers/branches'; import BranchBaselineSettingModal from './BranchBaselineSettingModal'; interface Props { branchLikes: T.BranchLike[]; component: T.Component; + inheritedSetting: T.NewCodePeriod; } interface State { @@ -78,10 +80,10 @@ export default class BranchList extends React.PureComponent<Props, State> { if (!newCodePeriod) { return b; } - const { type = 'PREVIOUS_VERSION', value = null } = newCodePeriod; + const { type = 'PREVIOUS_VERSION', value, effectiveValue } = newCodePeriod; return { ...b, - newCodePeriod: { type, value } + newCodePeriod: { type, value, effectiveValue } }; }); @@ -93,10 +95,7 @@ export default class BranchList extends React.PureComponent<Props, State> { ); } - updateBranchNewCodePeriod = ( - branch: string, - newSetting: { type: T.NewCodePeriodSettingType; value: string | null } | undefined - ) => { + updateBranchNewCodePeriod = (branch: string, newSetting: T.NewCodePeriod | undefined) => { const { branches } = this.state; const updated = branches.find(b => b.name === branch); @@ -110,10 +109,7 @@ export default class BranchList extends React.PureComponent<Props, State> { this.setState({ editedBranch: branch }); }; - closeEditModal = ( - branch?: string, - newSetting?: { type: T.NewCodePeriodSettingType; value: string | null } - ) => { + closeEditModal = (branch?: string, newSetting?: T.NewCodePeriod) => { if (branch) { this.setState({ branches: this.updateBranchNewCodePeriod(branch, newSetting), @@ -133,13 +129,19 @@ export default class BranchList extends React.PureComponent<Props, State> { }); } - renderNewCodePeriodSetting(newCodePeriod: { - type: T.NewCodePeriodSettingType; - value: string | null; - }) { + renderNewCodePeriodSetting(newCodePeriod: T.NewCodePeriod) { switch (newCodePeriod.type) { case 'SPECIFIC_ANALYSIS': - return `${translate('baseline.specific_analysis')}: ${newCodePeriod.value}`; + 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': @@ -166,6 +168,7 @@ export default class BranchList extends React.PureComponent<Props, State> { <thead> <tr> <th>{translate('branch_list.branch')}</th> + <th className="thin"> </th> <th className="thin nowrap huge-spacer-right"> {translate('branch_list.current_setting')} </th> @@ -182,13 +185,16 @@ export default class BranchList extends React.PureComponent<Props, State> { <div className="badge spacer-left">{translate('branches.main_branch')}</div> )} </td> - <td className="huge-spacer-right nowrap"> - {branch.newCodePeriod ? ( - this.renderNewCodePeriodSetting(branch.newCodePeriod) - ) : ( + <td> + {!branch.newCodePeriod && ( <span className="badge badge-info">{translate('default')}</span> )} </td> + <td className="huge-spacer-right nowrap"> + {branch.newCodePeriod + ? this.renderNewCodePeriodSetting(branch.newCodePeriod) + : this.renderNewCodePeriodSetting(this.props.inheritedSetting)} + </td> <td className="text-right"> <ActionsDropdown> <ActionsDropdownItem onClick={() => this.openEditModal(branch)}> 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 1a7a9759bcd..a29dfb03eba 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 @@ -29,7 +29,6 @@ export interface ProjectBaselineSelectorProps { currentSetting?: T.NewCodePeriodSettingType; currentSettingValue?: string | number; days: string; - generalSetting?: { type: T.NewCodePeriodSettingType; value?: string }; onSelectDays: (value: string) => void; onSelectSetting: (value: T.NewCodePeriodSettingType) => void; onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void; diff --git a/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx index 449e2932e2f..02cedc53b09 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx @@ -68,7 +68,7 @@ export class CategoriesList extends React.PureComponent<Props> { key, name: getCategoryName(key) })) - .concat(FIXED_CATEGORIES); + .concat(!this.props.component ? FIXED_CATEGORIES : []); const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase()); return ( <ul className="side-tabs-menu"> diff --git a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx index 9209014393f..3a2d3a956da 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx @@ -97,7 +97,7 @@ export class App extends React.PureComponent<Props & WithRouterProps, State> { /> </div> <div className="side-tabs-main"> - {selectedCategory === 'new_code_period' ? ( + {!this.props.component && selectedCategory === 'new_code_period' ? ( <NewCodePeriod /> ) : ( <CategoryDefinitionsList diff --git a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx index 28fd0b461b2..82f282f8701 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx @@ -90,7 +90,7 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { const { days, selected } = this.state; const type = selected; - const value = type === 'NUMBER_OF_DAYS' ? days : null; + const value = type === 'NUMBER_OF_DAYS' ? days : undefined; if (type) { this.setState({ saving: true, success: false }); diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-test.tsx index ac8d7740c8b..fd720c42546 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-test.tsx @@ -108,7 +108,7 @@ it('should submit correctly', async () => { wrapper.find('form').simulate('submit', { preventDefault }); expect(preventDefault).toBeCalledTimes(1); - expect(setNewCodePeriod).toBeCalledWith({ type: 'PREVIOUS_VERSION', value: null }); + expect(setNewCodePeriod).toBeCalledWith({ type: 'PREVIOUS_VERSION', value: undefined }); await waitAndUpdate(wrapper); expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected')); }); 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 8c02e687246..235ae322053 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1115,6 +1115,8 @@ project_activity.delete_analysis.question=Are you sure you want to delete this a project_activity.filter_events=Filter events project_activity.events.tooltip.edit=Edit this event project_activity.events.tooltip.delete=Delete this event +project_activity.new_code_period_start=New Code Period starts here +project_activity.new_code_period_start.help=The analysis before this mark is the baseline for New Code comparison project_activity.graphs.issues=Issues project_activity.graphs.coverage=Coverage |