aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2019-08-09 13:56:03 +0200
committerSonarTech <sonartech@sonarsource.com>2019-09-24 20:21:15 +0200
commit0d22031fffe8154061fb18addc8c426c74d78456 (patch)
treee6218ac008ad8ce55ac7a98efe1386a94824606a /server/sonar-web
parentff0e7573b8beb8321f1ab4024b819e2af16b7e9f (diff)
downloadsonarqube-0d22031fffe8154061fb18addc8c426c74d78456.tar.gz
sonarqube-0d22031fffe8154061fb18addc8c426c74d78456.zip
SONAR-11658 branch setting for the new code period
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/newCodePeriod.ts6
-rw-r--r--server/sonar-web/src/main/js/api/projectActivity.ts8
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css5
-rw-r--r--server/sonar-web/src/main/js/app/types.d.ts16
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/App-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BaselineSettingAnalysis-test.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchAnalysisList-test.tsx110
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchBaselineSettingModal-test.tsx120
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/BranchList-test.tsx117
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/App-test.tsx.snap16
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BaselineSettingAnalysis-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchAnalysisList-test.tsx.snap45
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap66
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap118
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx (renamed from server/sonar-web/src/main/js/api/baseline.ts)30
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx265
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx191
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx218
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/styles.css14
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx4
-rw-r--r--server/sonar-web/src/main/js/helpers/testMocks.ts12
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',