diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2019-08-30 17:31:15 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-09-24 20:21:17 +0200 |
commit | 3079907d1f13a509709c72a5dade87d129a4eb5c (patch) | |
tree | d3a79825cbf8ae25a3f473f5cfc07ffbfd36eba2 | |
parent | 45f8d835abdaf01287e2cf4a149b87ab2abfd156 (diff) | |
download | sonarqube-3079907d1f13a509709c72a5dade87d129a4eb5c.tar.gz sonarqube-3079907d1f13a509709c72a5dade87d129a4eb5c.zip |
SONAR-12430 Handle project baseline for Community Edition
25 files changed, 354 insertions, 161 deletions
diff --git a/server/sonar-web/src/main/js/api/newCodePeriod.ts b/server/sonar-web/src/main/js/api/newCodePeriod.ts index 1c21cefa735..da2b9d83ea7 100644 --- a/server/sonar-web/src/main/js/api/newCodePeriod.ts +++ b/server/sonar-web/src/main/js/api/newCodePeriod.ts @@ -23,7 +23,7 @@ import throwGlobalError from '../app/utils/throwGlobalError'; export function getNewCodePeriod(data?: { project?: string; branch?: string; -}): Promise<{ type: T.NewCodePeriodSettingType; inherited?: boolean; value?: string }> { +}): Promise<T.Omit<T.NewCodePeriod, 'effectiveValue'>> { return getJSON('/api/new_code_periods/show', data).catch(throwGlobalError); } 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 d2a89083b22..703d7b8db5b 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -52,15 +52,22 @@ declare namespace T { name: string; } - export interface Analysis { + interface BaseAnalysis { buildString?: string; - date: string; events: AnalysisEvent[]; key: string; manualNewCodePeriodBaseline?: boolean; projectVersion?: string; } + export interface Analysis extends BaseAnalysis { + date: string; + } + + export interface ParsedAnalysis extends BaseAnalysis { + date: Date; + } + export interface AnalysisEvent { category: string; description?: string; @@ -507,6 +514,7 @@ declare namespace T { type?: NewCodePeriodSettingType; value?: string; effectiveValue?: string; + inherited?: boolean; } export interface NewCodePeriodBranch { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx index c52f9217dce..468b80897af 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx @@ -26,13 +26,12 @@ import { hasHistoryData, isCustomGraph, MeasureHistory, - ParsedAnalysis, Serie } from '../utils'; import GraphHistory from './GraphHistory'; interface Props { - analyses: ParsedAnalysis[]; + analyses: T.ParsedAnalysis[]; eventFilter: string; graph: string; graphs: Serie[][]; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx index d6de9e60612..25171679c5c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx @@ -28,7 +28,6 @@ import DateFormatter from '../../../components/intl/DateFormatter'; import { activityQueryChanged, getAnalysesByVersionByDay, - ParsedAnalysis, Query, selectedDateQueryChanged } from '../utils'; @@ -37,7 +36,7 @@ import ProjectActivityAnalysis from './ProjectActivityAnalysis'; interface Props { addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>; addVersion: (analysis: string, version: string) => Promise<void>; - analyses: ParsedAnalysis[]; + analyses: T.ParsedAnalysis[]; analysesLoading: boolean; canAdmin?: boolean; canDeleteAnalyses?: boolean; @@ -157,14 +156,14 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro this.props.updateQuery({ selectedDate: date }); }; - shouldRenderBaselineMarker(analysis: ParsedAnalysis): boolean { + shouldRenderBaselineMarker(analysis: T.ParsedAnalysis): boolean { return Boolean( analysis.manualNewCodePeriodBaseline || (this.props.leakPeriodDate && isEqual(this.props.leakPeriodDate, analysis.date)) ); } - renderAnalysis(analysis: ParsedAnalysis) { + renderAnalysis(analysis: T.ParsedAnalysis) { const firstAnalysisKey = this.props.analyses[0].key; const selectedDate = this.props.query.selectedDate diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx index 515388f74d8..cc748a3041c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx @@ -28,7 +28,6 @@ import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; import { parseDate } from 'sonar-ui-common/helpers/dates'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import TimeFormatter from '../../../components/intl/TimeFormatter'; -import { ParsedAnalysis } from '../utils'; import Events from './Events'; import AddEventForm from './forms/AddEventForm'; import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; @@ -36,7 +35,7 @@ import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; interface Props { addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>; addVersion: (analysis: string, version: string) => Promise<void>; - analysis: ParsedAnalysis; + analysis: T.ParsedAnalysis; canAdmin?: boolean; canDeleteAnalyses?: boolean; canCreateVersion: boolean; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx index 8766d537f91..fef42d2314d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx @@ -23,7 +23,7 @@ import { parseDate } from 'sonar-ui-common/helpers/dates'; import { translate } from 'sonar-ui-common/helpers/l10n'; import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; -import { MeasureHistory, ParsedAnalysis, Query } from '../utils'; +import { MeasureHistory, Query } from '../utils'; import './projectActivity.css'; import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; import ProjectActivityGraphs from './ProjectActivityGraphs'; @@ -32,7 +32,7 @@ import ProjectActivityPageHeader from './ProjectActivityPageHeader'; interface Props { addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>; addVersion: (analysis: string, version: string) => Promise<void>; - analyses: ParsedAnalysis[]; + analyses: T.ParsedAnalysis[]; analysesLoading: boolean; changeEvent: (event: string, name: string) => Promise<void>; deleteAnalysis: (analysis: string) => Promise<void>; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx index 7283ba97355..ce0e6a6196a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx @@ -33,7 +33,6 @@ import { getProjectActivityGraph, isCustomGraph, MeasureHistory, - ParsedAnalysis, parseQuery, Query, serializeQuery, @@ -49,7 +48,7 @@ interface Props { } export interface State { - analyses: ParsedAnalysis[]; + analyses: T.ParsedAnalysis[]; analysesLoading: boolean; graphLoading: boolean; initialized: boolean; @@ -158,7 +157,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro analyses: analyses.map(analysis => ({ ...analysis, date: parseDate(analysis.date) - })) as ParsedAnalysis[], + })) as T.ParsedAnalysis[], paging })); }; @@ -204,8 +203,8 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro loadAllActivities = ( project: string, - prevResult?: { analyses: ParsedAnalysis[]; paging: T.Paging } - ): Promise<{ analyses: ParsedAnalysis[]; paging: T.Paging }> => { + prevResult?: { analyses: T.ParsedAnalysis[]; paging: T.Paging } + ): Promise<{ analyses: T.ParsedAnalysis[]; paging: T.Paging }> => { if ( prevResult && prevResult.paging.pageIndex * prevResult.paging.pageSize >= prevResult.paging.total diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx index 2f7ac01cbda..d2c510a4a51 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx @@ -29,7 +29,6 @@ import { historyQueryChanged, isCustomGraph, MeasureHistory, - ParsedAnalysis, Point, PROJECT_ACTIVITY_GRAPH, PROJECT_ACTIVITY_GRAPH_CUSTOM, @@ -42,7 +41,7 @@ import GraphsZoom from './GraphsZoom'; import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader'; interface Props { - analyses: ParsedAnalysis[]; + analyses: T.ParsedAnalysis[]; leakPeriodDate?: Date; loading: boolean; measuresHistory: MeasureHistory[]; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx index 3b0c9ff0e54..679cce88b1e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx @@ -20,12 +20,11 @@ import * as React from 'react'; import ConfirmModal from 'sonar-ui-common/components/controls/ConfirmModal'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import { ParsedAnalysis } from '../../utils'; interface Props { addEvent: (analysis: string, name: string, category?: string) => Promise<void>; addEventButtonText: string; - analysis: ParsedAnalysis; + analysis: T.ParsedAnalysis; onClose: () => void; } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.tsx index c3dee2cac2d..36fdc8d723d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.tsx @@ -20,10 +20,9 @@ import * as React from 'react'; import ConfirmModal from 'sonar-ui-common/components/controls/ConfirmModal'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import { ParsedAnalysis } from '../../utils'; interface Props { - analysis: ParsedAnalysis; + analysis: T.ParsedAnalysis; deleteAnalysis: (analysis: string) => Promise<void>; onClose: () => void; } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts index 7d8c774edd2..82dd6bdd1bc 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts @@ -32,8 +32,6 @@ import { } from 'sonar-ui-common/helpers/query'; import { get } from 'sonar-ui-common/helpers/storage'; -export type ParsedAnalysis = T.Omit<T.Analysis, 'date'> & { date: Date }; - export interface Query { category: string; customMetrics: string[]; @@ -193,12 +191,12 @@ export function getSeriesMetricType(series: Serie[]) { } interface AnalysesByDay { - byDay: T.Dict<ParsedAnalysis[]>; + byDay: T.Dict<T.ParsedAnalysis[]>; version: string | null; key: string | null; } -export function getAnalysesByVersionByDay(analyses: ParsedAnalysis[], query: Query) { +export function getAnalysesByVersionByDay(analyses: T.ParsedAnalysis[], query: Query) { return analyses.reduce<AnalysesByDay[]>((acc, analysis) => { let currentVersion = acc[acc.length - 1]; const versionEvent = analysis.events.find(event => event.category === 'VERSION'); 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 32b7edf81da..84c2a008476 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 @@ -93,6 +93,12 @@ it('should handle errors gracefully', async () => { function shallowRender(props: Partial<App['props']> = {}) { return shallow<App>( - <App branchLikes={[]} canAdmin={true} component={mockComponent()} {...props} /> + <App + branchLikes={[]} + branchesEnabled={true} + canAdmin={true} + component={mockComponent()} + {...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 index 230a2ec74c9..abe14e18235 100644 --- 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 @@ -88,26 +88,6 @@ it('should disable the save button when date is invalid', () => { ).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()).toBeUndefined(); - }); -}); - function shallowRender(props: Partial<BranchBaselineSettingModal['props']> = {}) { return shallow<BranchBaselineSettingModal>( <BranchBaselineSettingModal 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 index 87608358874..74384231bf2 100644 --- 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 @@ -25,6 +25,7 @@ import ProjectBaselineSelector, { it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); + expect(shallowRender({ branchesEnabled: false })).toMatchSnapshot(); }); it('should not show save button when unchanged', () => { @@ -32,7 +33,12 @@ it('should not show save button when unchanged', () => { currentSetting: 'PREVIOUS_VERSION', selected: 'PREVIOUS_VERSION' }); - expect(wrapper.find('SubmitButton')).toHaveLength(0); + expect( + wrapper + .find('SubmitButton') + .parent() + .hasClass('invisible') + ).toBe(true); }); it('should show save button when changed', () => { @@ -84,7 +90,10 @@ it('should disable the save button when date is invalid', () => { function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) { return shallow( <ProjectBaselineSelector + branchesEnabled={true} + component="" days="12" + onSelectAnalysis={jest.fn()} onSelectDays={jest.fn()} onSelectSetting={jest.fn()} onSubmit={jest.fn()} 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 index a9710d794a4..e9ffc30753c 100644 --- 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 @@ -17,7 +17,7 @@ exports[`should render correctly 1`] = ` onSubmit={[Function]} > <div - className="modal-body branch-baseline-setting-modal" + className="modal-body modal-container branch-baseline-setting-modal" > <div className="display-flex-row huge-spacer-bottom" 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 index c8fe6992237..d938d19be84 100644 --- 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 @@ -6,21 +6,96 @@ exports[`should render correctly 1`] = ` onSubmit={[MockFunction]} > <div - className="display-flex-row big-spacer-bottom" - role="radiogroup" + className="branch-baseline-setting-modal" > - <BaselineSettingPreviousVersion - onSelect={[MockFunction]} - selected={false} + <div + className="display-flex-row big-spacer-bottom" + role="radiogroup" + > + <BaselineSettingPreviousVersion + onSelect={[MockFunction]} + selected={false} + /> + <BaselineSettingDays + days="12" + isChanged={false} + isValid={false} + onChangeDays={[MockFunction]} + onSelect={[MockFunction]} + selected={false} + /> + </div> + </div> + <div + className="big-spacer-top invisible" + > + <p + className="spacer-bottom" + > + baseline.next_analysis_notice + </p> + <DeferredSpinner + className="spacer-right" + loading={false} + timeout={100} /> - <BaselineSettingDays - days="12" - isChanged={false} - isValid={true} - onChangeDays={[MockFunction]} - onSelect={[MockFunction]} - selected={false} + <SubmitButton + disabled={true} + > + save + </SubmitButton> + </div> +</form> +`; + +exports[`should render correctly 2`] = ` +<form + className="project-baseline-selector" + onSubmit={[MockFunction]} +> + <div + className="branch-baseline-setting-modal" + > + <div + className="display-flex-row big-spacer-bottom" + role="radiogroup" + > + <BaselineSettingPreviousVersion + onSelect={[MockFunction]} + selected={false} + /> + <BaselineSettingDays + days="12" + isChanged={false} + isValid={false} + onChangeDays={[MockFunction]} + onSelect={[MockFunction]} + selected={false} + /> + <BaselineSettingAnalysis + onSelect={[MockFunction]} + selected={false} + /> + </div> + </div> + <div + className="big-spacer-top invisible" + > + <p + className="spacer-bottom" + > + baseline.next_analysis_notice + </p> + <DeferredSpinner + className="spacer-right" + loading={false} + timeout={100} /> + <SubmitButton + disabled={true} + > + save + </SubmitButton> </div> </form> `; diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/utils-test.ts new file mode 100644 index 00000000000..c77f816b373 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/utils-test.ts @@ -0,0 +1,40 @@ +/* + * 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 { getSettingValue } from '../utils'; + +describe('getSettingValue', () => { + it('should work for Days', () => { + expect(getSettingValue({ analysis: 'analysis', days: '35', type: 'NUMBER_OF_DAYS' })).toBe( + '35' + ); + }); + + it('should work for Analysis', () => { + expect(getSettingValue({ analysis: 'analysis1', days: '35', type: 'SPECIFIC_ANALYSIS' })).toBe( + 'analysis1' + ); + }); + + it('should work for Previous version', () => { + expect( + getSettingValue({ analysis: 'analysis1', days: '35', type: 'PREVIOUS_VERSION' }) + ).toBeUndefined(); + }); +}); 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 c88e2fc9e64..816c0805985 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 @@ -25,16 +25,19 @@ 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 { getSettingValue } from '../utils'; import BranchList from './BranchList'; import ProjectBaselineSelector from './ProjectBaselineSelector'; interface Props { branchLikes: T.BranchLike[]; + branchesEnabled?: boolean; canAdmin?: boolean; component: T.Component; } interface State { + analysis?: string; currentSetting?: T.NewCodePeriodSettingType; currentSettingValue?: string; days: string; @@ -65,9 +68,34 @@ export default class App extends React.PureComponent<Props, State> { this.mounted = false; } + getUpdatedState(params: { + currentSetting?: T.NewCodePeriodSettingType; + currentSettingValue?: string; + generalSetting: T.NewCodePeriod; + }) { + const { currentSetting, currentSettingValue, generalSetting } = params; + + return { + loading: false, + currentSetting, + currentSettingValue, + generalSetting, + selected: currentSetting, + days: currentSetting === 'NUMBER_OF_DAYS' ? currentSettingValue || '30' : '', + analysis: (currentSetting === 'SPECIFIC_ANALYSIS' && currentSettingValue) || '' + }; + } + fetchLeakPeriodSetting() { this.setState({ loading: true }); - Promise.all([getNewCodePeriod(), getNewCodePeriod({ project: this.props.component.key })]).then( + + Promise.all([ + getNewCodePeriod(), + getNewCodePeriod({ + branch: this.props.branchesEnabled ? 'master' : undefined, + project: this.props.component.key + }) + ]).then( ([generalSetting, setting]) => { if (this.mounted) { if (!generalSetting.type) { @@ -75,22 +103,10 @@ export default class App extends React.PureComponent<Props, State> { } 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( + this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue }) + ); } }, () => { @@ -115,6 +131,8 @@ export default class App extends React.PureComponent<Props, State> { ); }; + handleSelectAnalysis = (analysis: T.ParsedAnalysis) => this.setState({ analysis: analysis.key }); + handleSelectDays = (days: string) => this.setState({ days }); handleSelectSetting = (selected?: T.NewCodePeriodSettingType) => this.setState({ selected }); @@ -123,10 +141,9 @@ export default class App extends React.PureComponent<Props, State> { e.preventDefault(); const { component } = this.props; - const { days, selected } = this.state; + const { analysis, days, selected: type } = this.state; - const type = selected; - const value = type === 'NUMBER_OF_DAYS' ? days : undefined; + const value = getSettingValue({ type, analysis, days }); if (type) { this.setState({ saving: true }); @@ -196,7 +213,9 @@ export default class App extends React.PureComponent<Props, State> { } render() { + const { branchLikes, branchesEnabled, component } = this.props; const { + analysis, currentSetting, days, generalSetting, @@ -213,21 +232,23 @@ export default class App extends React.PureComponent<Props, State> { <DeferredSpinner /> ) : ( <div className="panel panel-white"> - <h2>{translate('project_baseline.default_setting')}</h2> - <p>{translate('project_baseline.default_setting.description')}</p> + {branchesEnabled && ( + <> + <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}> + <Button className="little-spacer-bottom" onClick={this.resetSetting}> {translate('project_baseline.reset_to_general')} </Button> </> )} - <div className="spacer-top spacer-right medium"> + <div className="spacer-top medium"> <strong>{translate('project_baseline.general_setting')}: </strong> {this.renderGeneralSetting(generalSetting)} </div> @@ -235,19 +256,23 @@ export default class App extends React.PureComponent<Props, State> { )} <ProjectBaselineSelector + analysis={analysis} + branchesEnabled={branchesEnabled} + component={component.key} currentSetting={currentSetting} currentSettingValue={currentSettingValue} days={days} + onSelectAnalysis={this.handleSelectAnalysis} onSelectDays={this.handleSelectDays} onSelectSetting={this.handleSelectSetting} onSubmit={this.handleSubmit} saving={saving} selected={selected} /> - {generalSetting && ( + {generalSetting && branchesEnabled && ( <BranchList - branchLikes={this.props.branchLikes} - component={this.props.component} + branchLikes={branchLikes} + component={component} inheritedSetting={ currentSetting ? { 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 index 10205a7f932..1a38ec7f7f8 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/AppContainer.ts @@ -22,6 +22,7 @@ import { getAppState, Store } from '../../../store/rootReducer'; import App from './App'; const mapStateToProps = (state: Store) => ({ + branchesEnabled: getAppState(state).branchesEnabled, canAdmin: getAppState(state).canAdmin }); 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 index 4791f62ba6e..b2ed30fd434 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx @@ -30,17 +30,17 @@ 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'; +import { getAnalysesByVersionByDay } from '../../projectActivity/utils'; interface Props { analysis: string; branch: string; component: string; - onSelectAnalysis: (analysis: ParsedAnalysis) => void; + onSelectAnalysis: (analysis: T.ParsedAnalysis) => void; } interface State { - analyses: ParsedAnalysis[]; + analyses: T.ParsedAnalysis[]; loading: boolean; range: number; scroll: number; @@ -90,7 +90,7 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State analyses: result.analyses.map(analysis => ({ ...analysis, date: parseDate(analysis.date) - })) as ParsedAnalysis[], + })) as T.ParsedAnalysis[], loading: false }); }); 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 index b8dd08c6b66..0a180907f92 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx @@ -24,8 +24,7 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { toNotSoISOString } from 'sonar-ui-common/helpers/dates'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { setNewCodePeriod } from '../../../api/newCodePeriod'; -import { ParsedAnalysis } from '../../projectActivity/utils'; -import { validateDays } from '../utils'; +import { getSettingValue, validateSetting } from '../utils'; import BaselineSettingAnalysis from './BaselineSettingAnalysis'; import BaselineSettingDays from './BaselineSettingDays'; import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion'; @@ -73,24 +72,13 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop : null; } - getSettingValue() { - switch (this.state.selected) { - case 'NUMBER_OF_DAYS': - return this.state.days; - case 'SPECIFIC_ANALYSIS': - return this.state.analysis; - default: - return undefined; - } - } - handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => { e.preventDefault(); const { branch, component } = this.props; - const { analysisDate, selected: type } = this.state; + const { analysis, analysisDate, days, selected: type } = this.state; - const value = this.getSettingValue(); + const value = getSettingValue({ type, analysis, days }); if (type) { this.setState({ saving: true }); @@ -121,7 +109,7 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop requestClose = () => this.props.onClose(); - handleSelectAnalysis = (analysis: ParsedAnalysis) => + handleSelectAnalysis = (analysis: T.ParsedAnalysis) => this.setState({ analysis: analysis.key, analysisDate: analysis.date }); handleSelectDays = (days: string) => this.setState({ days }); @@ -132,20 +120,18 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop 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 currentSetting = branch.newCodePeriod && branch.newCodePeriod.type; + const currentSettingValue = branch.newCodePeriod && branch.newCodePeriod.value; - const isValid = - selected === 'PREVIOUS_VERSION' || - (selected === 'SPECIFIC_ANALYSIS' && analysis.length > 0) || - (selected === 'NUMBER_OF_DAYS' && validateDays(days)); + const { isChanged, isValid } = validateSetting({ + analysis, + currentSetting, + currentSettingValue, + days, + selected + }); return ( <Modal contentLabel={header} onRequestClose={this.requestClose} size="large"> @@ -153,7 +139,7 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop <h2>{header}</h2> </header> <form onSubmit={this.handleSubmit}> - <div className="modal-body branch-baseline-setting-modal"> + <div className="modal-body modal-container branch-baseline-setting-modal"> <div className="display-flex-row huge-spacer-bottom" role="radiogroup"> <BaselineSettingPreviousVersion isDefault={false} 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 index a29dfb03eba..5aabd1ad5ac 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx @@ -17,18 +17,25 @@ * 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 * 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 { validateSetting } from '../utils'; +import BaselineSettingAnalysis from './BaselineSettingAnalysis'; import BaselineSettingDays from './BaselineSettingDays'; import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion'; +import BranchAnalysisList from './BranchAnalysisList'; export interface ProjectBaselineSelectorProps { + analysis?: string; + branchesEnabled?: boolean; + component: string; currentSetting?: T.NewCodePeriodSettingType; - currentSettingValue?: string | number; + currentSettingValue?: string; days: string; + onSelectAnalysis: (analysis: T.ParsedAnalysis) => void; onSelectDays: (value: string) => void; onSelectSetting: (value: T.NewCodePeriodSettingType) => void; onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void; @@ -37,37 +44,62 @@ export interface ProjectBaselineSelectorProps { } export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) { - const { currentSetting, days, currentSettingValue, saving, selected } = props; + const { + analysis, + branchesEnabled, + component, + 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); + const { isChanged, isValid } = validateSetting({ + analysis, + currentSetting, + currentSettingValue, + days, + selected + }); 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 className="branch-baseline-setting-modal"> + <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'} + /> + {!branchesEnabled && ( + <BaselineSettingAnalysis + onSelect={props.onSelectSetting} + selected={selected === 'SPECIFIC_ANALYSIS'} + /> + )} </div> - )} + {selected === 'SPECIFIC_ANALYSIS' && ( + <BranchAnalysisList + analysis={analysis || ''} + branch="master" + component={component} + onSelectAnalysis={props.onSelectAnalysis} + /> + )} + </div> + <div className={classNames('big-spacer-top', { invisible: !isChanged })}> + <p className="spacer-bottom">{translate('baseline.next_analysis_notice')}</p> + <DeferredSpinner className="spacer-right" loading={saving} /> + <SubmitButton disabled={saving || !isValid || !isChanged}>{translate('save')}</SubmitButton> + </div> </form> ); } 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 06b52b4a9bc..9bffdfa35cc 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/styles.css +++ b/server/sonar-web/src/main/js/apps/projectBaseline/styles.css @@ -18,13 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -.project-baseline-selector { - height: 300px; +.project-baseline-selector > .branch-baseline-setting-modal { + max-height: 60vh; + padding-top: 2px; } .branch-baseline-setting-modal { - height: 60vh; - overflow: hidden; display: flex; flex-direction: column; } @@ -34,6 +33,7 @@ flex-direction: column; overflow: hidden; position: relative; + min-height: 200px; } .branch-analysis-list { @@ -100,10 +100,6 @@ 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; @@ -133,3 +129,7 @@ .project-activity-event-icon.OTHER { color: #442d1b; } + +.invisible { + visibility: hidden; +} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts index afaa962b3dc..69b7cfabda0 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts @@ -22,3 +22,44 @@ export function validateDays(days: string) { return !(days.length < 1 || isNaN(parsed) || parsed < 1 || String(parsed) !== days); } + +export function getSettingValue({ + analysis, + days, + type +}: { + analysis?: string; + days?: string; + type?: T.NewCodePeriodSettingType; +}) { + switch (type) { + case 'NUMBER_OF_DAYS': + return days; + case 'SPECIFIC_ANALYSIS': + return analysis; + default: + return undefined; + } +} + +export function validateSetting(state: { + analysis?: string; + currentSetting?: T.NewCodePeriodSettingType; + currentSettingValue?: string; + days: string; + selected?: T.NewCodePeriodSettingType; +}) { + const { analysis = '', currentSetting, currentSettingValue, days, selected } = state; + + const isChanged = + selected !== currentSetting || + (selected === 'NUMBER_OF_DAYS' && 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 { isChanged, isValid }; +} diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 0b159181b44..c667b17d2d1 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -22,7 +22,6 @@ import { Location } from 'history'; import { InjectedRouter } from 'react-router'; import { createStore, Store } from 'redux'; import { DocumentationEntry } from '../apps/documentation/utils'; -import { ParsedAnalysis } from '../apps/projectActivity/utils'; import { Profile } from '../apps/quality-profiles/types'; export function mockAlmApplication(overrides: Partial<T.AlmApplication> = {}): T.AlmApplication { @@ -61,7 +60,7 @@ export function mockAnalysis(overrides: Partial<T.Analysis> = {}): T.Analysis { }; } -export function mockParsedAnalysis(overrides: Partial<ParsedAnalysis> = {}): ParsedAnalysis { +export function mockParsedAnalysis(overrides: Partial<T.ParsedAnalysis> = {}): T.ParsedAnalysis { return { date: new Date('2017-03-01T09:37:01+0100'), events: [], |