@@ -39,3 +39,9 @@ export function setNewCodePeriod(data: { | |||
export function resetNewCodePeriod(data: { project?: string; branch?: string }): Promise<void> { | |||
return post('/api/new_code_periods/unset', data).catch(throwGlobalError); | |||
} | |||
export function listBranchesNewCodePeriod(data: { | |||
project: string; | |||
}): Promise<{ newCodePeriods: T.NewCodePeriodBranch[] }> { | |||
return getJSON('/api/new_code_periods/list', data).catch(throwGlobalError); | |||
} |
@@ -21,7 +21,13 @@ import { getJSON, post, postJSON, RequestData } from 'sonar-ui-common/helpers/re | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function getProjectActivity( | |||
data: { project: string; category?: string; p?: number; ps?: number } & T.BranchParameters | |||
data: { | |||
project: string; | |||
category?: string; | |||
from?: string; | |||
p?: number; | |||
ps?: number; | |||
} & T.BranchParameters | |||
): Promise<{ analyses: T.Analysis[]; paging: T.Paging }> { | |||
return getJSON('/api/project_analyses/search', data).catch(throwGlobalError); | |||
} |
@@ -174,6 +174,11 @@ td.big-spacer-top { | |||
padding-top: 16px !important; | |||
} | |||
td.huge-spacer-right, | |||
th.huge-spacer-right { | |||
padding-right: 40px !important; | |||
} | |||
.pull-left { | |||
float: left !important; | |||
} |
@@ -114,6 +114,13 @@ declare namespace T { | |||
export type BranchType = 'LONG' | 'SHORT'; | |||
export interface BranchWithNewCodePeriod extends Branch { | |||
newCodePeriod?: { | |||
type: NewCodePeriodSettingType; | |||
value: string | null; | |||
}; | |||
} | |||
export interface Breadcrumb { | |||
key: string; | |||
name: string; | |||
@@ -499,10 +506,17 @@ declare namespace T { | |||
qualityGate?: string; | |||
} | |||
export interface NewCodePeriodBranch { | |||
projectKey: string; | |||
branchKey: string; | |||
inherited?: boolean; | |||
type?: NewCodePeriodSettingType; | |||
value?: string | null; | |||
} | |||
export type NewCodePeriodSettingType = | |||
| 'PREVIOUS_VERSION' | |||
| 'NUMBER_OF_DAYS' | |||
| 'DATE' | |||
| 'SPECIFIC_ANALYSIS'; | |||
export interface Notification { |
@@ -361,7 +361,7 @@ exports[`should show the correct admin options 2`] = ` | |||
addEventButtonText="project_activity.add_custom_event" | |||
analysis={ | |||
Object { | |||
"date": 2017-03-01T08:36:01.000Z, | |||
"date": 2017-03-01T08:37:01.000Z, | |||
"events": Array [], | |||
"key": "foo", | |||
"projectVersion": "1.0", | |||
@@ -442,7 +442,7 @@ exports[`should show the correct admin options 3`] = ` | |||
<RemoveAnalysisForm | |||
analysis={ | |||
Object { | |||
"date": 2017-03-01T08:36:01.000Z, | |||
"date": 2017-03-01T08:37:01.000Z, | |||
"events": Array [], | |||
"key": "foo", | |||
"projectVersion": "1.0", | |||
@@ -526,7 +526,7 @@ exports[`should show the correct admin options 4`] = ` | |||
addEventButtonText="project_activity.add_version" | |||
analysis={ | |||
Object { | |||
"date": 2017-03-01T08:36:01.000Z, | |||
"date": 2017-03-01T08:37:01.000Z, | |||
"events": Array [], | |||
"key": "foo", | |||
"projectVersion": "1.0", |
@@ -92,5 +92,7 @@ it('should handle errors gracefully', async () => { | |||
}); | |||
function shallowRender(props: Partial<App['props']> = {}) { | |||
return shallow<App>(<App canAdmin={true} component={mockComponent()} {...props} />); | |||
return shallow<App>( | |||
<App branchLikes={[]} canAdmin={true} component={mockComponent()} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,41 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 BaselineSettingAnalysis, { Props } from '../components/BaselineSettingAnalysis'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should callback when clicked', () => { | |||
const onSelect = jest.fn(); | |||
const wrapper = shallowRender({ onSelect, selected: false }); | |||
wrapper | |||
.find('RadioCard') | |||
.first() | |||
.simulate('click'); | |||
expect(onSelect).toHaveBeenCalledWith('SPECIFIC_ANALYSIS'); | |||
}); | |||
function shallowRender(props: Partial<Props> = {}) { | |||
return shallow(<BaselineSettingAnalysis onSelect={jest.fn()} selected={true} {...props} />); | |||
} |
@@ -0,0 +1,110 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { subDays } from 'date-fns'; | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { toShortNotSoISOString } from 'sonar-ui-common/helpers/dates'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { getProjectActivity } from '../../../api/projectActivity'; | |||
import { mockAnalysis, mockAnalysisEvent } from '../../../helpers/testMocks'; | |||
import BranchAnalysisList from '../components/BranchAnalysisList'; | |||
jest.mock('../../../api/projectActivity', () => ({ | |||
getProjectActivity: jest.fn().mockResolvedValue({ | |||
analyses: [] | |||
}) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', async () => { | |||
(getProjectActivity as jest.Mock).mockResolvedValueOnce({ | |||
analyses: [ | |||
mockAnalysis({ | |||
key: '4', | |||
date: '2017-03-02T10:36:01+0100', | |||
projectVersion: '4.2' | |||
}), | |||
mockAnalysis({ | |||
key: '3', | |||
date: '2017-03-02T09:36:01+0100', | |||
events: [mockAnalysisEvent()], | |||
projectVersion: '4.2' | |||
}), | |||
mockAnalysis({ | |||
key: '2', | |||
events: [ | |||
mockAnalysisEvent(), | |||
mockAnalysisEvent({ category: 'VERSION', qualityGate: undefined }) | |||
], | |||
projectVersion: '4.1' | |||
}), | |||
mockAnalysis({ key: '1', projectVersion: '4.1' }) | |||
] | |||
}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should reload analyses after range change', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.instance().handleRangeChange({ value: 20 }); | |||
expect(getProjectActivity).toBeCalledWith({ | |||
branch: 'master', | |||
project: 'project1', | |||
from: toShortNotSoISOString(subDays(new Date(), 20)) | |||
}); | |||
}); | |||
it('should register the badge nodes', () => { | |||
const wrapper = shallowRender(); | |||
const element = document.createElement('div'); | |||
wrapper.instance().registerBadgeNode('4.3')(element); | |||
expect(element.getAttribute('originOffsetTop')).not.toBeNull(); | |||
}); | |||
it('should handle scroll', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.instance().handleScroll({ currentTarget: { scrollTop: 12 } } as any); | |||
expect(wrapper.state('scroll')).toBe(12); | |||
}); | |||
function shallowRender(props: Partial<BranchAnalysisList['props']> = {}) { | |||
return shallow<BranchAnalysisList>( | |||
<BranchAnalysisList | |||
analysis="analysis1" | |||
branch="master" | |||
component="project1" | |||
onSelectAnalysis={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,120 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { setNewCodePeriod } from '../../../api/newCodePeriod'; | |||
import { mockMainBranch } from '../../../helpers/testMocks'; | |||
import BranchBaselineSettingModal from '../components/BranchBaselineSettingModal'; | |||
jest.mock('../../../api/newCodePeriod', () => ({ | |||
setNewCodePeriod: jest.fn().mockResolvedValue({}) | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should display the branch analysis list when necessary', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ selected: 'SPECIFIC_ANALYSIS' }); | |||
expect(wrapper.find('BranchAnalysisList')).toHaveLength(1); | |||
}); | |||
it('should save correctly', async () => { | |||
const branch = mockMainBranch({ name: 'branchname' }); | |||
const component = 'compKey'; | |||
const wrapper = shallowRender({ | |||
branch, | |||
component | |||
}); | |||
wrapper.setState({ analysis: 'analysis572893', selected: 'SPECIFIC_ANALYSIS' }); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleSubmit(mockEvent()); | |||
await new Promise(setImmediate); | |||
expect(setNewCodePeriod).toHaveBeenCalledWith({ | |||
project: component, | |||
type: 'SPECIFIC_ANALYSIS', | |||
value: 'analysis572893', | |||
branch: 'branchname' | |||
}); | |||
}); | |||
it('should disable the save button when saving', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ saving: true }); | |||
expect( | |||
wrapper | |||
.find('SubmitButton') | |||
.first() | |||
.prop('disabled') | |||
).toBe(true); | |||
}); | |||
it('should disable the save button when date is invalid', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ days: 'asdf' }); | |||
expect( | |||
wrapper | |||
.find('SubmitButton') | |||
.first() | |||
.prop('disabled') | |||
).toBe(true); | |||
}); | |||
describe('getSettingValue', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ analysis: 'analysis1', days: '35' }); | |||
it('should work for Days', () => { | |||
wrapper.setState({ selected: 'NUMBER_OF_DAYS' }); | |||
expect(wrapper.instance().getSettingValue()).toBe('35'); | |||
}); | |||
it('should work for Analysis', () => { | |||
wrapper.setState({ selected: 'SPECIFIC_ANALYSIS' }); | |||
expect(wrapper.instance().getSettingValue()).toBe('analysis1'); | |||
}); | |||
it('should work for Previous version', () => { | |||
wrapper.setState({ selected: 'PREVIOUS_VERSION' }); | |||
expect(wrapper.instance().getSettingValue()).toBeNull(); | |||
}); | |||
}); | |||
function shallowRender(props: Partial<BranchBaselineSettingModal['props']> = {}) { | |||
return shallow<BranchBaselineSettingModal>( | |||
<BranchBaselineSettingModal | |||
branch={mockMainBranch()} | |||
component="compKey" | |||
onClose={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,117 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { listBranchesNewCodePeriod, resetNewCodePeriod } from '../../../api/newCodePeriod'; | |||
import { | |||
mockComponent, | |||
mockLongLivingBranch, | |||
mockMainBranch, | |||
mockPullRequest, | |||
mockShortLivingBranch | |||
} from '../../../helpers/testMocks'; | |||
import BranchList from '../components/BranchList'; | |||
jest.mock('../../../api/newCodePeriod', () => ({ | |||
listBranchesNewCodePeriod: jest.fn().mockResolvedValue({ newCodePeriods: [] }), | |||
resetNewCodePeriod: jest.fn().mockResolvedValue(null) | |||
})); | |||
it('should render correctly', async () => { | |||
(listBranchesNewCodePeriod as jest.Mock).mockResolvedValueOnce({ | |||
newCodePeriods: [ | |||
{ | |||
projectKey: '', | |||
branchKey: 'master', | |||
type: 'NUMBER_OF_DAYS', | |||
value: '27' | |||
} | |||
] | |||
}); | |||
const wrapper = shallowRender({ | |||
branchLikes: [ | |||
mockMainBranch(), | |||
mockLongLivingBranch(), | |||
mockShortLivingBranch(), | |||
mockPullRequest() | |||
] | |||
}); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('branches')).toHaveLength(2); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should handle reset', () => { | |||
const component = mockComponent(); | |||
const wrapper = shallowRender({ component }); | |||
wrapper.instance().resetToDefault('master'); | |||
expect(resetNewCodePeriod).toBeCalledWith({ | |||
project: component.key, | |||
branch: 'master' | |||
}); | |||
}); | |||
it('should toggle popup', async () => { | |||
const wrapper = shallowRender({ branchLikes: [mockMainBranch(), mockLongLivingBranch()] }); | |||
wrapper.setState({ editedBranch: mockMainBranch() }); | |||
await waitAndUpdate(wrapper); | |||
const nodes = wrapper.find('BranchBaselineSettingModal'); | |||
expect(nodes).toHaveLength(1); | |||
expect(nodes.first().prop('branch')).toEqual(mockMainBranch()); | |||
wrapper.instance().closeEditModal('master', { type: 'NUMBER_OF_DAYS', value: '23' }); | |||
expect(wrapper.find('BranchBaselineSettingModal')).toHaveLength(0); | |||
expect(wrapper.state('branches').find(b => b.name === 'master')).toEqual({ | |||
analysisDate: '2018-01-01', | |||
isMain: true, | |||
name: 'master', | |||
newCodePeriod: { | |||
type: 'NUMBER_OF_DAYS', | |||
value: '23' | |||
} | |||
}); | |||
}); | |||
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', value: null }) | |||
).toBe('baseline.previous_version'); | |||
expect( | |||
wrapper.instance().renderNewCodePeriodSetting({ type: 'SPECIFIC_ANALYSIS', value: 'A85835' }) | |||
).toBe('baseline.specific_analysis: A85835'); | |||
}); | |||
function shallowRender(props: Partial<BranchList['props']> = {}) { | |||
return shallow<BranchList>( | |||
<BranchList branchLikes={[]} component={mockComponent()} {...props} /> | |||
); | |||
} |
@@ -20,11 +20,13 @@ exports[`should render correctly 1`] = ` | |||
id="project_baseline.page.description" | |||
values={ | |||
Object { | |||
"link": <a | |||
href="/documentation/user-guide/fixing-the-water-leak/" | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/user-guide/fixing-the-water-leak/" | |||
> | |||
project_baseline.page.description.link | |||
</a>, | |||
</Link>, | |||
} | |||
} | |||
/> | |||
@@ -34,11 +36,13 @@ exports[`should render correctly 1`] = ` | |||
id="project_baseline.page.description2" | |||
values={ | |||
Object { | |||
"link": <a | |||
href="/admin/settings?category=new_code_period" | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/admin/settings?category=new_code_period" | |||
> | |||
project_baseline.page.description2.link | |||
</a>, | |||
</Link>, | |||
} | |||
} | |||
/> |
@@ -0,0 +1,15 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<RadioCard | |||
onClick={[Function]} | |||
selected={true} | |||
title="baseline.specific_analysis" | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
baseline.specific_analysis.description | |||
</p> | |||
</RadioCard> | |||
`; |
@@ -0,0 +1,45 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
baseline.analysis_from | |||
<Select | |||
autoBlur={true} | |||
className="input-medium spacer-left" | |||
clearable={false} | |||
onChange={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "baseline.branch_analyses.ranges.30days", | |||
"value": 30, | |||
}, | |||
Object { | |||
"label": "baseline.branch_analyses.ranges.allTime", | |||
"value": 0, | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value={0} | |||
/> | |||
</div> | |||
<div | |||
className="branch-analysis-list-wrapper" | |||
> | |||
<div | |||
className="bordered branch-analysis-list" | |||
onScroll={[Function]} | |||
> | |||
<div | |||
className="big-spacer-top big-spacer-bottom strong" | |||
> | |||
baseline.no_analyses | |||
</div> | |||
</div> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -0,0 +1,66 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 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 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} | |||
/> | |||
</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> | |||
`; |
@@ -0,0 +1,118 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<table | |||
className="data zebra" | |||
> | |||
<thead> | |||
<tr> | |||
<th> | |||
branch_list.branch | |||
</th> | |||
<th | |||
className="thin nowrap huge-spacer-right" | |||
> | |||
branch_list.current_setting | |||
</th> | |||
<th | |||
className="thin nowrap" | |||
> | |||
branch_list.edit_settings | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr | |||
key="master" | |||
> | |||
<td | |||
className="nowrap" | |||
> | |||
<BranchIcon | |||
branchLike={ | |||
Object { | |||
"analysisDate": "2018-01-01", | |||
"isMain": true, | |||
"name": "master", | |||
"newCodePeriod": Object { | |||
"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 | |||
key="branch-6.7" | |||
> | |||
<td | |||
className="nowrap" | |||
> | |||
<BranchIcon | |||
branchLike={ | |||
Object { | |||
"analysisDate": "2018-01-01", | |||
"isMain": false, | |||
"name": "branch-6.7", | |||
"type": "LONG", | |||
} | |||
} | |||
className="little-spacer-right" | |||
/> | |||
branch-6.7 | |||
</td> | |||
<td | |||
className="huge-spacer-right nowrap" | |||
> | |||
<span | |||
className="badge badge-info" | |||
> | |||
default | |||
</span> | |||
</td> | |||
<td | |||
className="text-right" | |||
> | |||
<ActionsDropdown> | |||
<ActionsDropdownItem | |||
onClick={[Function]} | |||
> | |||
edit | |||
</ActionsDropdownItem> | |||
</ActionsDropdown> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</Fragment> | |||
`; |
@@ -19,14 +19,17 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod'; | |||
import '../styles.css'; | |||
import BranchList from './BranchList'; | |||
import ProjectBaselineSelector from './ProjectBaselineSelector'; | |||
interface Props { | |||
branchLikes: T.BranchLike[]; | |||
canAdmin?: boolean; | |||
component: T.Component; | |||
} | |||
@@ -156,9 +159,9 @@ export default class App extends React.PureComponent<Props, State> { | |||
id="project_baseline.page.description" | |||
values={{ | |||
link: ( | |||
<a href="/documentation/user-guide/fixing-the-water-leak/"> | |||
<Link to="/documentation/user-guide/fixing-the-water-leak/"> | |||
{translate('project_baseline.page.description.link')} | |||
</a> | |||
</Link> | |||
) | |||
}} | |||
/> | |||
@@ -169,9 +172,9 @@ export default class App extends React.PureComponent<Props, State> { | |||
id="project_baseline.page.description2" | |||
values={{ | |||
link: ( | |||
<a href="/admin/settings?category=new_code_period"> | |||
<Link to="/admin/settings?category=new_code_period"> | |||
{translate('project_baseline.page.description2.link')} | |||
</a> | |||
</Link> | |||
) | |||
}} | |||
/> | |||
@@ -242,6 +245,8 @@ export default class App extends React.PureComponent<Props, State> { | |||
saving={saving} | |||
selected={selected} | |||
/> | |||
<BranchList branchLikes={this.props.branchLikes} component={this.props.component} /> | |||
</div> | |||
)} | |||
</div> |
@@ -17,18 +17,22 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getJSON } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import * as React from 'react'; | |||
import RadioCard from 'sonar-ui-common/components/controls/RadioCard'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
export function getBranchAnalyses( | |||
data: { | |||
project: string; | |||
category?: string; | |||
from?: string; | |||
to?: string; | |||
p?: number; | |||
ps?: number; | |||
} & T.BranchParameters | |||
) { | |||
return getJSON('/api/project_analyses/search', data).catch(throwGlobalError); | |||
export interface Props { | |||
onSelect: (selection: T.NewCodePeriodSettingType) => void; | |||
selected: boolean; | |||
} | |||
export default function BaselineSettingAnalysis({ onSelect, selected }: Props) { | |||
return ( | |||
<RadioCard | |||
onClick={() => onSelect('SPECIFIC_ANALYSIS')} | |||
selected={selected} | |||
title={translate('baseline.specific_analysis')}> | |||
<p className="big-spacer-bottom">{translate('baseline.specific_analysis.description')}</p> | |||
</RadioCard> | |||
); | |||
} |
@@ -0,0 +1,265 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { subDays } from 'date-fns'; | |||
import { throttle } from 'lodash'; | |||
import * as React from 'react'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
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 { getProjectActivity } from '../../../api/projectActivity'; | |||
import DateFormatter from '../../../components/intl/DateFormatter'; | |||
import TimeFormatter from '../../../components/intl/TimeFormatter'; | |||
import Events from '../../projectActivity/components/Events'; | |||
import { getAnalysesByVersionByDay, ParsedAnalysis } from '../../projectActivity/utils'; | |||
interface Props { | |||
analysis: string; | |||
branch: string; | |||
component: string; | |||
onSelectAnalysis: (analysis: string) => void; | |||
} | |||
interface State { | |||
analyses: ParsedAnalysis[]; | |||
loading: boolean; | |||
range: number; | |||
scroll: number; | |||
} | |||
export default class BranchAnalysisList extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
badges: T.Dict<HTMLDivElement> = {}; | |||
state: State = { | |||
analyses: [], | |||
loading: true, | |||
range: 30, | |||
scroll: 0 | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.updateScroll = throttle(this.updateScroll, 20); | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchAnalyses(true); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchAnalyses(initial = false) { | |||
const { analysis, branch, component } = this.props; | |||
const { range } = this.state; | |||
this.setState({ loading: true }); | |||
return getProjectActivity({ | |||
branch, | |||
project: component, | |||
from: range ? toShortNotSoISOString(subDays(new Date(), range)) : undefined | |||
}).then((result: { analyses: T.Analysis[] }) => { | |||
// If the selected analysis wasn't found in the default 30 days range, redo the search | |||
if (initial && analysis && !result.analyses.find(a => a.key === analysis)) { | |||
this.handleRangeChange({ value: 0 }); | |||
return; | |||
} | |||
this.setState({ | |||
analyses: result.analyses.map(analysis => ({ | |||
...analysis, | |||
date: parseDate(analysis.date) | |||
})) as ParsedAnalysis[], | |||
loading: false | |||
}); | |||
}); | |||
} | |||
handleScroll = (e: React.SyntheticEvent<HTMLDivElement>) => { | |||
if (e.currentTarget) { | |||
this.updateScroll(e.currentTarget.scrollTop); | |||
} | |||
}; | |||
updateScroll = (scroll: number) => { | |||
this.setState({ scroll }); | |||
}; | |||
registerBadgeNode = (version: string) => (el: HTMLDivElement) => { | |||
if (el) { | |||
if (!el.getAttribute('originOffsetTop')) { | |||
el.setAttribute('originOffsetTop', String(el.offsetTop)); | |||
} | |||
this.badges[version] = el; | |||
} | |||
}; | |||
shouldStick = (version: string, index: number) => { | |||
const badge = this.badges[version]; | |||
return ( | |||
badge && Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + 18 + index * 2 | |||
); | |||
}; | |||
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 { analyses, loading, range } = this.state; | |||
const byVersionByDay = getAnalysesByVersionByDay(analyses, { | |||
category: '', | |||
customMetrics: [], | |||
graph: '', | |||
project: this.props.component | |||
}); | |||
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}> | |||
{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, idx) | |||
})} | |||
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: false | |||
})} | |||
data-date={parseDate(analysis.date).valueOf()} | |||
key={analysis.key} | |||
onClick={() => this.props.onSelectAnalysis(analysis.key)}> | |||
<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 | |||
analysis={analysis.key} | |||
changeEvent={() => Promise.resolve()} | |||
deleteEvent={() => Promise.resolve()} | |||
events={analysis.events} | |||
isFirst={analyses[0].key === analysis.key} | |||
/> | |||
)} | |||
<div className="analysis-selection-button"> | |||
<i | |||
className={classNames('icon-radio', { | |||
'is-checked': analysis.key === this.props.analysis | |||
})} | |||
/> | |||
</div> | |||
</li> | |||
))} | |||
</ul> | |||
</li> | |||
))} | |||
</ul> | |||
</li> | |||
); | |||
})} | |||
</ul> | |||
)} | |||
</div> | |||
</div> | |||
</> | |||
); | |||
} | |||
} |
@@ -0,0 +1,191 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { 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 { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { setNewCodePeriod } from '../../../api/newCodePeriod'; | |||
import { validateDays } from '../utils'; | |||
import BaselineSettingAnalysis from './BaselineSettingAnalysis'; | |||
import BaselineSettingDays from './BaselineSettingDays'; | |||
import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion'; | |||
import BranchAnalysisList from './BranchAnalysisList'; | |||
interface Props { | |||
branch: T.BranchWithNewCodePeriod; | |||
component: string; | |||
onClose: ( | |||
branch?: string, | |||
newSetting?: { type: T.NewCodePeriodSettingType; value: string | null } | |||
) => void; | |||
} | |||
interface State { | |||
analysis: string; | |||
days: string; | |||
saving: boolean; | |||
selected?: T.NewCodePeriodSettingType; | |||
} | |||
export default class BranchBaselineSettingModal extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
analysis: this.getValueFromProps('SPECIFIC_ANALYSIS') || '', | |||
days: this.getValueFromProps('NUMBER_OF_DAYS') || '30', | |||
saving: false, | |||
selected: this.props.branch.newCodePeriod && this.props.branch.newCodePeriod.type | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
getValueFromProps(type: T.NewCodePeriodSettingType) { | |||
return this.props.branch.newCodePeriod && this.props.branch.newCodePeriod.type === type | |||
? this.props.branch.newCodePeriod.value | |||
: null; | |||
} | |||
getSettingValue() { | |||
switch (this.state.selected) { | |||
case 'NUMBER_OF_DAYS': | |||
return this.state.days; | |||
case 'SPECIFIC_ANALYSIS': | |||
return this.state.analysis; | |||
default: | |||
return null; | |||
} | |||
} | |||
handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => { | |||
e.preventDefault(); | |||
const { branch, component } = this.props; | |||
const { selected: type } = this.state; | |||
const value = this.getSettingValue(); | |||
if (type) { | |||
this.setState({ saving: true }); | |||
setNewCodePeriod({ | |||
project: component, | |||
type, | |||
value, | |||
branch: branch.name | |||
}).then( | |||
() => { | |||
this.setState({ | |||
saving: false | |||
}); | |||
this.props.onClose(branch.name, { type, value }); | |||
}, | |||
() => { | |||
this.setState({ | |||
saving: false | |||
}); | |||
} | |||
); | |||
} | |||
}; | |||
requestClose = () => this.props.onClose(); | |||
handleSelectAnalysis = (analysis: string) => this.setState({ analysis }); | |||
handleSelectDays = (days: string) => this.setState({ days }); | |||
handleSelectSetting = (selected: T.NewCodePeriodSettingType) => this.setState({ selected }); | |||
render() { | |||
const { branch } = this.props; | |||
const { analysis, days, saving, selected } = this.state; | |||
const currentSetting = branch.newCodePeriod && branch.newCodePeriod.type; | |||
const currentSettingValue = branch.newCodePeriod && branch.newCodePeriod.value; | |||
const header = translateWithParameters('baseline.new_code_period_for_branch_x', branch.name); | |||
const isChanged = | |||
selected !== currentSetting || | |||
(selected === 'NUMBER_OF_DAYS' && String(days) !== currentSettingValue) || | |||
(selected === 'SPECIFIC_ANALYSIS' && analysis !== currentSettingValue); | |||
const isValid = | |||
selected === 'PREVIOUS_VERSION' || | |||
(selected === 'SPECIFIC_ANALYSIS' && analysis.length > 0) || | |||
(selected === 'NUMBER_OF_DAYS' && validateDays(days)); | |||
return ( | |||
<Modal contentLabel={header} onRequestClose={this.requestClose} size="large"> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-body branch-baseline-setting-modal"> | |||
<div className="display-flex-row huge-spacer-bottom" role="radiogroup"> | |||
<BaselineSettingPreviousVersion | |||
isDefault={false} | |||
onSelect={this.handleSelectSetting} | |||
selected={selected === 'PREVIOUS_VERSION'} | |||
/> | |||
<BaselineSettingDays | |||
days={days} | |||
isChanged={isChanged} | |||
isValid={isValid} | |||
onChangeDays={this.handleSelectDays} | |||
onSelect={this.handleSelectSetting} | |||
selected={selected === 'NUMBER_OF_DAYS'} | |||
/> | |||
<BaselineSettingAnalysis | |||
onSelect={this.handleSelectSetting} | |||
selected={selected === 'SPECIFIC_ANALYSIS'} | |||
/> | |||
</div> | |||
{selected === 'SPECIFIC_ANALYSIS' && ( | |||
<BranchAnalysisList | |||
analysis={analysis} | |||
branch={branch.name} | |||
component={this.props.component} | |||
onSelectAnalysis={this.handleSelectAnalysis} | |||
/> | |||
)} | |||
</div> | |||
<footer className="modal-foot"> | |||
<DeferredSpinner className="spacer-right" loading={saving} /> | |||
<SubmitButton disabled={!isChanged || saving || !isValid}> | |||
{translate('save')} | |||
</SubmitButton> | |||
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} | |||
} |
@@ -0,0 +1,218 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 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 { isLongLivingBranch, isMainBranch, sortBranchesAsTree } from '../../../helpers/branches'; | |||
import BranchBaselineSettingModal from './BranchBaselineSettingModal'; | |||
interface Props { | |||
branchLikes: T.BranchLike[]; | |||
component: T.Component; | |||
} | |||
interface State { | |||
branches: T.BranchWithNewCodePeriod[]; | |||
editedBranch?: T.BranchWithNewCodePeriod; | |||
loading: boolean; | |||
} | |||
export default class BranchList extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
branches: [], | |||
loading: true | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchBranches(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
sortAndFilterBranches(branchLikes: T.BranchLike[] = []) { | |||
return sortBranchesAsTree( | |||
branchLikes.filter(b => isMainBranch(b) || isLongLivingBranch(b)) | |||
) as T.Branch[]; | |||
} | |||
fetchBranches() { | |||
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 newCodePeriod = newCodePeriods.find(ncp => ncp.branchKey === b.name); | |||
if (!newCodePeriod) { | |||
return b; | |||
} | |||
const { type = 'PREVIOUS_VERSION', value = null } = newCodePeriod; | |||
return { | |||
...b, | |||
newCodePeriod: { type, value } | |||
}; | |||
}); | |||
this.setState({ branches: branchesWithBaseline, loading: false }); | |||
}, | |||
() => { | |||
this.setState({ loading: false }); | |||
} | |||
); | |||
} | |||
updateBranchNewCodePeriod = ( | |||
branch: string, | |||
newSetting: { type: T.NewCodePeriodSettingType; value: string | null } | undefined | |||
) => { | |||
const { branches } = this.state; | |||
const updated = branches.find(b => b.name === branch); | |||
if (updated) { | |||
updated.newCodePeriod = newSetting; | |||
} | |||
return branches.slice(0); | |||
}; | |||
openEditModal = (branch: T.BranchWithNewCodePeriod) => { | |||
this.setState({ editedBranch: branch }); | |||
}; | |||
closeEditModal = ( | |||
branch?: string, | |||
newSetting?: { type: T.NewCodePeriodSettingType; value: string | null } | |||
) => { | |||
if (branch) { | |||
this.setState({ | |||
branches: this.updateBranchNewCodePeriod(branch, newSetting), | |||
editedBranch: undefined | |||
}); | |||
} else { | |||
this.setState({ editedBranch: undefined }); | |||
} | |||
}; | |||
resetToDefault(branch: string) { | |||
return resetNewCodePeriod({ | |||
project: this.props.component.key, | |||
branch | |||
}).then(() => { | |||
this.setState({ branches: this.updateBranchNewCodePeriod(branch, undefined) }); | |||
}); | |||
} | |||
renderNewCodePeriodSetting(newCodePeriod: { | |||
type: T.NewCodePeriodSettingType; | |||
value: string | null; | |||
}) { | |||
switch (newCodePeriod.type) { | |||
case 'SPECIFIC_ANALYSIS': | |||
return `${translate('baseline.specific_analysis')}: ${newCodePeriod.value}`; | |||
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 { branches, editedBranch, loading } = this.state; | |||
if (branches.length < 1) { | |||
return null; | |||
} | |||
if (loading) { | |||
return <DeferredSpinner />; | |||
} | |||
return ( | |||
<> | |||
<table className="data zebra"> | |||
<thead> | |||
<tr> | |||
<th>{translate('branch_list.branch')}</th> | |||
<th className="thin nowrap huge-spacer-right"> | |||
{translate('branch_list.current_setting')} | |||
</th> | |||
<th className="thin nowrap">{translate('branch_list.edit_settings')}</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{branches.map(branch => ( | |||
<tr key={branch.name}> | |||
<td className="nowrap"> | |||
<BranchIcon 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) | |||
) : ( | |||
<span className="badge badge-info">{translate('default')}</span> | |||
)} | |||
</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> | |||
))} | |||
</tbody> | |||
</table> | |||
{editedBranch && ( | |||
<BranchBaselineSettingModal | |||
branch={editedBranch} | |||
component={this.props.component.key} | |||
onClose={this.closeEditModal} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -54,15 +54,15 @@ | |||
} | |||
.branch-analysis-day { | |||
margin-top: 8px; | |||
margin-bottom: 24px; | |||
margin-top: var(--gridSize); | |||
margin-bottom: calc(3 * var(--gridSize)); | |||
} | |||
.branch-analysis { | |||
display: flex; | |||
justify-content: space-between; | |||
cursor: pointer; | |||
padding: 8px; | |||
padding: var(--gridSize); | |||
border-top: 1px solid var(--barBorderColor); | |||
border-bottom: 1px solid var(--barBorderColor); | |||
} | |||
@@ -85,18 +85,18 @@ | |||
.branch-analysis-version-badge { | |||
margin-left: -12px; | |||
padding-top: 8px; | |||
padding-bottom: 8px; | |||
padding-top: var(--gridSize); | |||
padding-bottom: var(--gridSize); | |||
background-color: white; | |||
} | |||
.branch-analysis-version-badge.sticky, | |||
.branch-analysis-version-badge.first { | |||
position: absolute; | |||
top: 25px; | |||
top: 1px; | |||
left: 13px; | |||
right: 16px; | |||
padding-top: 24px; | |||
padding-top: calc(3 * var(--gridSize)); | |||
z-index: var(--belowNormalZIndex); | |||
} | |||
@@ -28,8 +28,6 @@ import BaselineSettingDays from '../../projectBaseline/components/BaselineSettin | |||
import BaselineSettingPreviousVersion from '../../projectBaseline/components/BaselineSettingPreviousVersion'; | |||
import { validateDays } from '../../projectBaseline/utils'; | |||
interface Props {} | |||
interface State { | |||
currentSetting?: T.NewCodePeriodSettingType; | |||
days: string; | |||
@@ -42,7 +40,7 @@ interface State { | |||
const DEFAULT_SETTING = 'PREVIOUS_VERSION'; | |||
export default class NewCodePeriod extends React.PureComponent<Props, State> { | |||
export default class NewCodePeriod extends React.PureComponent<{}, State> { | |||
mounted = false; | |||
state: State = { | |||
loading: true, |
@@ -51,9 +51,19 @@ export function mockAlmOrganization(overrides: Partial<T.AlmOrganization> = {}): | |||
}; | |||
} | |||
export function mockAnalysis(overrides: Partial<T.Analysis> = {}): T.Analysis { | |||
return { | |||
date: '2017-03-01T09:36:01+0100', | |||
events: [], | |||
key: 'foo', | |||
projectVersion: '1.0', | |||
...overrides | |||
}; | |||
} | |||
export function mockParsedAnalysis(overrides: Partial<ParsedAnalysis> = {}): ParsedAnalysis { | |||
return { | |||
date: new Date('2017-03-01T09:36:01+0100'), | |||
date: new Date('2017-03-01T09:37:01+0100'), | |||
events: [], | |||
key: 'foo', | |||
projectVersion: '1.0', |
@@ -545,6 +545,8 @@ project_baseline.page.description2=You can adjust this setting globally in {link | |||
project_baseline.page.description2.link=General Settings | |||
project_baseline.default_setting=Project default setting | |||
project_baseline.default_setting.description=This setting is the default for all branches of the project | |||
project_baseline.general_setting=General setting | |||
project_baseline.reset_to_general=Reset to general setting | |||
baseline.previous_version=Previous version | |||
baseline.previous_version.description=The New Code Period will begin with the analysis following the previous version. | |||
@@ -563,6 +565,7 @@ branch_list.branch=Branch | |||
branch_list.current_setting=Current setting | |||
branch_list.current_baseline=Current Baseline | |||
branch_list.edit_settings=Edit settings | |||
branch_list.default_setting=Project setting | |||
baseline.new_code_period_for_branch_x=New Code Period for {0} | |||