aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2019-08-09 13:54:33 +0200
committerSonarTech <sonartech@sonarsource.com>2019-09-24 20:21:15 +0200
commitff0e7573b8beb8321f1ab4024b819e2af16b7e9f (patch)
treeee986b63032509893193dd33ecf8c06dbbd08743
parent035fbea593106d85be9e8a9a823f3ed8ae2fc024 (diff)
downloadsonarqube-ff0e7573b8beb8321f1ab4024b819e2af16b7e9f.tar.gz
sonarqube-ff0e7573b8beb8321f1ab4024b819e2af16b7e9f.zip
SONAR-11630 Project setting for new code period
-rw-r--r--server/sonar-web/src/main/js/api/baseline.ts34
-rw-r--r--server/sonar-web/src/main/js/api/newCodePeriod.ts4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/App-test.tsx96
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/ProjectBaselineSelector-test.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/App-test.tsx.snap51
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap26
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx250
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts28
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx74
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/routes.ts28
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/styles.css135
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/utils.ts24
14 files changed, 864 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/api/baseline.ts b/server/sonar-web/src/main/js/api/baseline.ts
new file mode 100644
index 00000000000..b94a76ef64c
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/baseline.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { getJSON } from 'sonar-ui-common/helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
+
+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);
+}
diff --git a/server/sonar-web/src/main/js/api/newCodePeriod.ts b/server/sonar-web/src/main/js/api/newCodePeriod.ts
index d198d171c71..1aa0f57eb50 100644
--- a/server/sonar-web/src/main/js/api/newCodePeriod.ts
+++ b/server/sonar-web/src/main/js/api/newCodePeriod.ts
@@ -35,3 +35,7 @@ export function setNewCodePeriod(data: {
}): Promise<void> {
return post('/api/new_code_periods/set', data).catch(throwGlobalError);
}
+
+export function resetNewCodePeriod(data: { project?: string; branch?: string }): Promise<void> {
+ return post('/api/new_code_periods/unset', data).catch(throwGlobalError);
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
index a1c17cb4098..83bd2377ccc 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
@@ -35,6 +35,7 @@ import { isSonarCloud } from '../../../../helpers/system';
const SETTINGS_URLS = [
'/project/admin',
+ '/project/baseline',
'/project/branches',
'/project/settings',
'/project/quality_profiles',
@@ -223,6 +224,7 @@ export class ComponentNavMenu extends React.PureComponent<Props> {
return [
this.renderSettingsLink(),
this.renderBranchesLink(),
+ this.renderBaselineLink(),
this.renderProfilesLink(),
this.renderQualityGateLink(),
this.renderCustomMeasuresLink(),
@@ -271,6 +273,18 @@ export class ComponentNavMenu extends React.PureComponent<Props> {
);
}
+ renderBaselineLink() {
+ return (
+ <li key="baseline">
+ <Link
+ activeClassName="active"
+ to={{ pathname: '/project/baseline', query: { id: this.props.component.key } }}>
+ {translate('project_baseline.page')}
+ </Link>
+ </li>
+ );
+ }
+
renderProfilesLink() {
if (!this.getConfiguration().showQualityProfiles) {
return null;
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx
index 10802cfced5..1eb7843fbcc 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx
@@ -50,6 +50,7 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes';
import portfolioRoutes from '../../apps/portfolio/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
+import projectBaselineRoutes from '../../apps/projectBaseline/routes';
import projectBranchesRoutes from '../../apps/projectBranches/routes';
import projectQualityGateRoutes from '../../apps/projectQualityGate/routes';
import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes';
@@ -270,6 +271,10 @@ export default function startReactApp(
childRoutes={backgroundTasksRoutes}
/>
<RouteWithChildRoutes
+ path="project/baseline"
+ childRoutes={projectBaselineRoutes}
+ />
+ <RouteWithChildRoutes
path="project/branches"
childRoutes={projectBranchesRoutes}
/>
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
new file mode 100644
index 00000000000..f7bc5af26f9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/App-test.tsx
@@ -0,0 +1,96 @@
+/*
+ * 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 { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod';
+import { mockComponent, mockEvent } from '../../../helpers/testMocks';
+import App from '../components/App';
+
+jest.mock('../../../api/newCodePeriod', () => ({
+ getNewCodePeriod: jest.fn().mockResolvedValue({}),
+ resetNewCodePeriod: jest.fn().mockResolvedValue({}),
+ setNewCodePeriod: jest.fn().mockResolvedValue({})
+}));
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should not display reset button if project setting is not set', () => {
+ const wrapper = shallowRender();
+
+ expect(wrapper.find('Button')).toHaveLength(0);
+});
+
+it('should display reset button if project setting is set', async () => {
+ (getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '27' });
+
+ const wrapper = shallowRender();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('Button')).toHaveLength(1);
+});
+
+it('should reset the setting correctly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ wrapper.instance().resetSetting();
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state('currentSetting')).toBeUndefined();
+ expect(wrapper.state('selected')).toBeUndefined();
+});
+
+it('should save correctly', async () => {
+ const component = mockComponent();
+ const wrapper = shallowRender({ component });
+ await waitAndUpdate(wrapper);
+ wrapper.setState({ selected: 'NUMBER_OF_DAYS', days: '23' });
+ wrapper.instance().handleSubmit(mockEvent());
+ await waitAndUpdate(wrapper);
+ expect(setNewCodePeriod).toHaveBeenCalledWith({
+ project: component.key,
+ type: 'NUMBER_OF_DAYS',
+ value: '23'
+ });
+ expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected'));
+});
+
+it('should handle errors gracefully', async () => {
+ (getNewCodePeriod as jest.Mock).mockRejectedValue('error');
+ (setNewCodePeriod as jest.Mock).mockRejectedValue('error');
+ (resetNewCodePeriod as jest.Mock).mockRejectedValue('error');
+
+ const wrapper = shallowRender();
+ wrapper.setState({ selected: 'PREVIOUS_VERSION' });
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state('loading')).toBe(false);
+ wrapper.instance().resetSetting();
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state('saving')).toBe(false);
+ wrapper.instance().handleSubmit(mockEvent());
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state('saving')).toBe(false);
+});
+
+function shallowRender(props: Partial<App['props']> = {}) {
+ return shallow<App>(<App canAdmin={true} component={mockComponent()} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/ProjectBaselineSelector-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/ProjectBaselineSelector-test.tsx
new file mode 100644
index 00000000000..87608358874
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/ProjectBaselineSelector-test.tsx
@@ -0,0 +1,95 @@
+/*
+ * 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 ProjectBaselineSelector, {
+ ProjectBaselineSelectorProps
+} from '../components/ProjectBaselineSelector';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should not show save button when unchanged', () => {
+ const wrapper = shallowRender({
+ currentSetting: 'PREVIOUS_VERSION',
+ selected: 'PREVIOUS_VERSION'
+ });
+ expect(wrapper.find('SubmitButton')).toHaveLength(0);
+});
+
+it('should show save button when changed', () => {
+ const wrapper = shallowRender({ currentSetting: 'PREVIOUS_VERSION', selected: 'NUMBER_OF_DAYS' });
+ expect(wrapper.find('SubmitButton')).toHaveLength(1);
+});
+
+it('should show save button when value changed', () => {
+ const wrapper = shallowRender({
+ currentSetting: 'NUMBER_OF_DAYS',
+ currentSettingValue: '23',
+ days: '25',
+ selected: 'NUMBER_OF_DAYS'
+ });
+ expect(wrapper.find('SubmitButton')).toHaveLength(1);
+});
+
+it('should disable the save button when saving', () => {
+ const wrapper = shallowRender({
+ currentSetting: 'NUMBER_OF_DAYS',
+ currentSettingValue: '25',
+ saving: true,
+ selected: 'PREVIOUS_VERSION'
+ });
+
+ expect(
+ wrapper
+ .find('SubmitButton')
+ .first()
+ .prop('disabled')
+ ).toBe(true);
+});
+
+it('should disable the save button when date is invalid', () => {
+ const wrapper = shallowRender({
+ currentSetting: 'PREVIOUS_VERSION',
+ days: 'hello',
+ selected: 'NUMBER_OF_DAYS'
+ });
+
+ expect(
+ wrapper
+ .find('SubmitButton')
+ .first()
+ .prop('disabled')
+ ).toBe(true);
+});
+
+function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) {
+ return shallow(
+ <ProjectBaselineSelector
+ days="12"
+ onSelectDays={jest.fn()}
+ onSelectSetting={jest.fn()}
+ onSubmit={jest.fn()}
+ saving={false}
+ {...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
new file mode 100644
index 00000000000..6c5fffb9d42
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/App-test.tsx.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="page page-limited"
+>
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ project_baseline.page
+ </h1>
+ <p
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="project_baseline.page.description"
+ id="project_baseline.page.description"
+ values={
+ Object {
+ "link": <a
+ href="/documentation/user-guide/fixing-the-water-leak/"
+ >
+ project_baseline.page.description.link
+ </a>,
+ }
+ }
+ />
+ <br />
+ <FormattedMessage
+ defaultMessage="project_baseline.page.description2"
+ id="project_baseline.page.description2"
+ values={
+ Object {
+ "link": <a
+ href="/admin/settings?category=new_code_period"
+ >
+ project_baseline.page.description2.link
+ </a>,
+ }
+ }
+ />
+ </p>
+ </header>
+ <DeferredSpinner
+ timeout={100}
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
new file mode 100644
index 00000000000..c8fe6992237
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<form
+ className="project-baseline-selector"
+ onSubmit={[MockFunction]}
+>
+ <div
+ className="display-flex-row big-spacer-bottom"
+ role="radiogroup"
+ >
+ <BaselineSettingPreviousVersion
+ onSelect={[MockFunction]}
+ selected={false}
+ />
+ <BaselineSettingDays
+ days="12"
+ isChanged={false}
+ isValid={true}
+ onChangeDays={[MockFunction]}
+ onSelect={[MockFunction]}
+ selected={false}
+ />
+ </div>
+</form>
+`;
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
new file mode 100644
index 00000000000..791adbaa344
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
@@ -0,0 +1,250 @@
+/*
+ * 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 { FormattedMessage } from 'react-intl';
+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 ProjectBaselineSelector from './ProjectBaselineSelector';
+
+interface Props {
+ canAdmin?: boolean;
+ component: T.Component;
+}
+
+interface State {
+ currentSetting?: T.NewCodePeriodSettingType;
+ currentSettingValue?: string | number;
+ days: string;
+ generalSetting?: { type: T.NewCodePeriodSettingType; value?: string };
+ loading: boolean;
+ saving: boolean;
+ selected?: T.NewCodePeriodSettingType;
+}
+
+const DEFAULT_GENERAL_SETTING: { type: T.NewCodePeriodSettingType } = {
+ type: 'PREVIOUS_VERSION'
+};
+
+export default class App extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ days: '30',
+ loading: true,
+ saving: false
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchLeakPeriodSetting();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchLeakPeriodSetting() {
+ this.setState({ loading: true });
+ Promise.all([getNewCodePeriod(), getNewCodePeriod({ project: this.props.component.key })]).then(
+ ([generalSetting, setting]) => {
+ if (this.mounted) {
+ if (!generalSetting.type) {
+ generalSetting = DEFAULT_GENERAL_SETTING;
+ }
+ const currentSettingValue = setting.value;
+ const currentSetting = setting.inherited ? undefined : setting.type || 'PREVIOUS_VERSION';
+ const newState = {
+ loading: false,
+ currentSetting,
+ currentSettingValue,
+ generalSetting,
+ selected: currentSetting
+ };
+
+ if (currentSetting === 'NUMBER_OF_DAYS') {
+ this.setState({
+ days: currentSettingValue || '30',
+ ...newState
+ });
+ } else {
+ this.setState(newState);
+ }
+ }
+ },
+ () => {
+ this.setState({ loading: false });
+ }
+ );
+ }
+
+ resetSetting = () => {
+ this.setState({ saving: true });
+ resetNewCodePeriod({ project: this.props.component.key }).then(
+ () => {
+ this.setState({
+ saving: false,
+ currentSetting: undefined,
+ selected: undefined
+ });
+ },
+ () => {
+ this.setState({ saving: false });
+ }
+ );
+ };
+
+ handleSelectDays = (days: string) => this.setState({ days });
+
+ handleSelectSetting = (selected?: T.NewCodePeriodSettingType) => this.setState({ selected });
+
+ handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+ e.preventDefault();
+
+ const { component } = this.props;
+ const { days, selected } = this.state;
+
+ const type = selected;
+ const value = type === 'NUMBER_OF_DAYS' ? days : null;
+
+ if (type) {
+ this.setState({ saving: true });
+ setNewCodePeriod({
+ project: component.key,
+ type,
+ value
+ }).then(
+ () => {
+ this.setState({
+ saving: false,
+ currentSetting: type,
+ currentSettingValue: value || undefined
+ });
+ },
+ () => {
+ this.setState({ saving: false });
+ }
+ );
+ }
+ };
+
+ renderHeader() {
+ return (
+ <header className="page-header">
+ <h1 className="page-title">{translate('project_baseline.page')}</h1>
+ <p className="page-description">
+ <FormattedMessage
+ defaultMessage={translate('project_baseline.page.description')}
+ id="project_baseline.page.description"
+ values={{
+ link: (
+ <a href="/documentation/user-guide/fixing-the-water-leak/">
+ {translate('project_baseline.page.description.link')}
+ </a>
+ )
+ }}
+ />
+ <br />
+ {this.props.canAdmin && (
+ <FormattedMessage
+ defaultMessage={translate('project_baseline.page.description2')}
+ id="project_baseline.page.description2"
+ values={{
+ link: (
+ <a href="/admin/settings?category=new_code_period">
+ {translate('project_baseline.page.description2.link')}
+ </a>
+ )
+ }}
+ />
+ )}
+ </p>
+ </header>
+ );
+ }
+
+ renderGeneralSetting(generalSetting: { type: T.NewCodePeriodSettingType; value?: string }) {
+ if (generalSetting.type === 'NUMBER_OF_DAYS') {
+ return `${translate('baseline.number_days')} (${translateWithParameters(
+ 'duration.days',
+ generalSetting.value || '?'
+ )})`;
+ } else {
+ return translate('baseline.previous_version');
+ }
+ }
+
+ render() {
+ const {
+ currentSetting,
+ days,
+ generalSetting,
+ loading,
+ currentSettingValue,
+ saving,
+ selected
+ } = this.state;
+
+ return (
+ <div className="page page-limited">
+ {this.renderHeader()}
+ {loading ? (
+ <DeferredSpinner />
+ ) : (
+ <div className="panel panel-white">
+ <h2>{translate('project_baseline.default_setting')}</h2>
+ <p>{translate('project_baseline.default_setting.description')}</p>
+
+ {generalSetting && (
+ <div className="text-right spacer-bottom">
+ {currentSetting && (
+ <>
+ <Button
+ className="spacer-right little-spacer-bottom"
+ onClick={this.resetSetting}>
+ {translate('project_baseline.reset_to_general')}
+ </Button>
+ </>
+ )}
+ <div className="spacer-top spacer-right medium">
+ <strong>{translate('project_baseline.general_setting')}: </strong>
+ {this.renderGeneralSetting(generalSetting)}
+ </div>
+ </div>
+ )}
+
+ <ProjectBaselineSelector
+ currentSetting={currentSetting}
+ currentSettingValue={currentSettingValue}
+ days={days}
+ generalSetting={generalSetting}
+ onSelectDays={this.handleSelectDays}
+ onSelectSetting={this.handleSelectSetting}
+ onSubmit={this.handleSubmit}
+ saving={saving}
+ selected={selected}
+ />
+ </div>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts
new file mode 100644
index 00000000000..10205a7f932
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { getAppState, Store } from '../../../store/rootReducer';
+import App from './App';
+
+const mapStateToProps = (state: Store) => ({
+ canAdmin: getAppState(state).canAdmin
+});
+
+export default connect(mapStateToProps)(App);
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
new file mode 100644
index 00000000000..1a7a9759bcd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { validateDays } from '../utils';
+import BaselineSettingDays from './BaselineSettingDays';
+import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion';
+
+export interface ProjectBaselineSelectorProps {
+ currentSetting?: T.NewCodePeriodSettingType;
+ currentSettingValue?: string | number;
+ days: string;
+ generalSetting?: { type: T.NewCodePeriodSettingType; value?: string };
+ onSelectDays: (value: string) => void;
+ onSelectSetting: (value: T.NewCodePeriodSettingType) => void;
+ onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void;
+ saving: boolean;
+ selected?: T.NewCodePeriodSettingType;
+}
+
+export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
+ const { currentSetting, days, currentSettingValue, saving, selected } = props;
+
+ const isChanged =
+ selected !== currentSetting ||
+ (selected === 'NUMBER_OF_DAYS' && String(days) !== currentSettingValue);
+
+ const isValid = selected !== 'NUMBER_OF_DAYS' || validateDays(days);
+
+ return (
+ <form className="project-baseline-selector" onSubmit={props.onSubmit}>
+ <div className="display-flex-row big-spacer-bottom" role="radiogroup">
+ <BaselineSettingPreviousVersion
+ onSelect={props.onSelectSetting}
+ selected={selected === 'PREVIOUS_VERSION'}
+ />
+ <BaselineSettingDays
+ days={days}
+ isChanged={isChanged}
+ isValid={isValid}
+ onChangeDays={props.onSelectDays}
+ onSelect={props.onSelectSetting}
+ selected={selected === 'NUMBER_OF_DAYS'}
+ />
+ </div>
+ {isChanged && (
+ <div>
+ <p className="spacer-bottom">{translate('baseline.next_analysis_notice')}</p>
+ <DeferredSpinner className="spacer-right" loading={saving} />
+ <SubmitButton disabled={saving || !isValid}>{translate('save')}</SubmitButton>
+ </div>
+ )}
+ </form>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts b/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts
new file mode 100644
index 00000000000..cefd6cdcedd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/routes.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { lazyLoad } from 'sonar-ui-common/components/lazyLoad';
+
+const routes = [
+ {
+ indexRoute: { component: lazyLoad(() => import('./components/AppContainer')) }
+ }
+];
+
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/styles.css b/server/sonar-web/src/main/js/apps/projectBaseline/styles.css
new file mode 100644
index 00000000000..b52f851a742
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/styles.css
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+
+.project-baseline-selector {
+ height: 300px;
+}
+
+.branch-baseline-setting-modal {
+ height: 60vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.branch-analysis-list-wrapper {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ position: relative;
+}
+
+.branch-analysis-list {
+ overflow-y: auto;
+ padding-left: 12px;
+ padding-right: 15px;
+ min-height: 50px;
+}
+
+.branch-analysis-list > ul {
+ padding-top: 52px;
+}
+
+.branch-analysis-date {
+ margin-bottom: 16px;
+ font-size: 15px;
+ font-weight: bold;
+}
+
+.branch-analysis-day {
+ margin-top: 8px;
+ margin-bottom: 24px;
+}
+
+.branch-analysis {
+ display: flex;
+ justify-content: space-between;
+ cursor: pointer;
+ padding: 8px;
+ border-top: 1px solid var(--barBorderColor);
+ border-bottom: 1px solid var(--barBorderColor);
+}
+
+.branch-analysis + .branch-analysis {
+ border-top: none;
+}
+
+.branch-analysis:hover {
+ background-color: var(--lightBlue);
+}
+
+.branch-analysis > .project-activity-events {
+ flex: 1 0 50%;
+}
+
+.branch-analysis-time {
+ width: 150px;
+}
+
+.branch-analysis-version-badge {
+ margin-left: -12px;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ background-color: white;
+}
+
+.branch-analysis-version-badge.sticky,
+.branch-analysis-version-badge.first {
+ position: absolute;
+ top: 25px;
+ left: 13px;
+ right: 16px;
+ padding-top: 24px;
+ z-index: var(--belowNormalZIndex);
+}
+
+.branch-analysis-version-badge.sticky + .branch-analysis-days-list {
+ padding-top: 36px;
+}
+
+.branch-analysis-version-badge .badge {
+ max-width: 385px;
+ border-radius: 0 2px 2px 0;
+ font-weight: bold;
+ font-size: var(--smallFontSize);
+ letter-spacing: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.project-activity-event-icon.VERSION {
+ color: var(--blue);
+}
+
+.project-activity-event-icon.QUALITY_GATE {
+ color: var(--purple);
+}
+
+.project-activity-event-icon.QUALITY_PROFILE {
+ color: #cccccc;
+}
+
+.project-activity-event-icon.DEFINITION_CHANGE {
+ color: #33a759;
+}
+
+.project-activity-event-icon.OTHER {
+ color: #442d1b;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
new file mode 100644
index 00000000000..afaa962b3dc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+export function validateDays(days: string) {
+ const parsed = parseInt(days, 10);
+
+ return !(days.length < 1 || isNaN(parsed) || parsed < 1 || String(parsed) !== days);
+}