diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2019-08-09 13:56:03 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-09-24 20:21:15 +0200 |
commit | 0d22031fffe8154061fb18addc8c426c74d78456 (patch) | |
tree | e6218ac008ad8ce55ac7a98efe1386a94824606a /server/sonar-web | |
parent | ff0e7573b8beb8321f1ab4024b819e2af16b7e9f (diff) | |
download | sonarqube-0d22031fffe8154061fb18addc8c426c74d78456.tar.gz sonarqube-0d22031fffe8154061fb18addc8c426c74d78456.zip |
SONAR-11658 branch setting for the new code period
Diffstat (limited to 'server/sonar-web')
23 files changed, 1400 insertions, 40 deletions
diff --git a/server/sonar-web/src/main/js/api/newCodePeriod.ts b/server/sonar-web/src/main/js/api/newCodePeriod.ts index 1aa0f57eb50..8043702a12b 100644 --- a/server/sonar-web/src/main/js/api/newCodePeriod.ts +++ b/server/sonar-web/src/main/js/api/newCodePeriod.ts @@ -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); +} diff --git a/server/sonar-web/src/main/js/api/projectActivity.ts b/server/sonar-web/src/main/js/api/projectActivity.ts index 05d4cd3626c..20aa8e8e89b 100644 --- a/server/sonar-web/src/main/js/api/projectActivity.ts +++ b/server/sonar-web/src/main/js/api/projectActivity.ts @@ -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); } diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 31650acaf14..d209ecbd8fb 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -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; } 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 4a371a86a99..23a911a076b 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -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 { 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 60706a012c2..722f614714e 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 @@ -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", diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/App-test.tsx index f7bc5af26f9..32b7edf81da 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/App-test.tsx @@ -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} /> + ); } diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BaselineSettingAnalysis-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BaselineSettingAnalysis-test.tsx new file mode 100644 index 00000000000..384ba11b597 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BaselineSettingAnalysis-test.tsx @@ -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} />); +} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchAnalysisList-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchAnalysisList-test.tsx new file mode 100644 index 00000000000..992853b7ec9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchAnalysisList-test.tsx @@ -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} + /> + ); +} 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 new file mode 100644 index 00000000000..a30c435de00 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchBaselineSettingModal-test.tsx @@ -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} + /> + ); +} 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 new file mode 100644 index 00000000000..73cff093697 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchList-test.tsx @@ -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} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/App-test.tsx.snap index 6c5fffb9d42..3cd10c0762c 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/App-test.tsx.snap @@ -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>, } } /> diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BaselineSettingAnalysis-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BaselineSettingAnalysis-test.tsx.snap new file mode 100644 index 00000000000..a5dc6125502 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BaselineSettingAnalysis-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchAnalysisList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchAnalysisList-test.tsx.snap new file mode 100644 index 00000000000..e215412fa0a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchAnalysisList-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap new file mode 100644 index 00000000000..a9710d794a4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap @@ -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> +`; 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 new file mode 100644 index 00000000000..51582805875 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap @@ -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> +`; 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 791adbaa344..992ffd8fc20 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 @@ -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> diff --git a/server/sonar-web/src/main/js/api/baseline.ts b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx index b94a76ef64c..9cb63f366af 100644 --- a/server/sonar-web/src/main/js/api/baseline.ts +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx @@ -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> + ); } 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 new file mode 100644 index 00000000000..c3803cacf14 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx @@ -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> + </> + ); + } +} 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 new file mode 100644 index 00000000000..095595ed2ff --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx @@ -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> + ); + } +} 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 new file mode 100644 index 00000000000..691247fac7d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx @@ -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} + /> + )} + </> + ); + } +} 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 b52f851a742..06b52b4a9bc 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/styles.css +++ b/server/sonar-web/src/main/js/apps/projectBaseline/styles.css @@ -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); } 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 997d61bf437..28fd0b461b2 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 @@ -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, diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 95542914134..4498df5013e 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -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', |