diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2019-08-09 13:54:33 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-09-24 20:21:15 +0200 |
commit | ff0e7573b8beb8321f1ab4024b819e2af16b7e9f (patch) | |
tree | ee986b63032509893193dd33ecf8c06dbbd08743 | |
parent | 035fbea593106d85be9e8a9a823f3ed8ae2fc024 (diff) | |
download | sonarqube-ff0e7573b8beb8321f1ab4024b819e2af16b7e9f.tar.gz sonarqube-ff0e7573b8beb8321f1ab4024b819e2af16b7e9f.zip |
SONAR-11630 Project setting for new code period
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); +} |