From: Wouter Admiraal Date: Fri, 11 Jun 2021 08:33:13 +0000 (+0200) Subject: SONAR-14935 Prompt users for next steps once the first analysis is finished X-Git-Tag: 9.0.0.45539~89 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ef084e20eb019c2a8f37b4e7827c5307bc1e2e5d;p=sonarqube.git SONAR-14935 Prompt users for next steps once the first analysis is finished --- diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx index 7ea3332157a..c1ccd55b86d 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx @@ -42,6 +42,7 @@ import { extractStatusConditionsFromApplicationStatusChildProject, extractStatusConditionsFromProjectStatus } from '../../../helpers/qualityGates'; +import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; import { ApplicationPeriod } from '../../../types/application'; import { Branch, BranchLike } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; @@ -54,12 +55,15 @@ import BranchOverviewRenderer from './BranchOverviewRenderer'; interface Props { branch?: Branch; + branchesEnabled?: boolean; component: T.Component; + projectBinding?: ProjectAlmBindingResponse; } interface State { analyses?: T.Analysis[]; appLeak?: ApplicationPeriod; + detectedCIOnLastAnalysis?: boolean; graph: GraphType; loadingHistory?: boolean; loadingStatus?: boolean; @@ -71,6 +75,7 @@ interface State { } export const BRANCH_OVERVIEW_ACTIVITY_GRAPH = 'sonar_branch_overview.graph'; +export const NO_CI_DETECTED = 'undetected'; // Get all history data over the past year. const FROM_DATE = toNotSoISOString(new Date().setFullYear(new Date().getFullYear() - 1)); @@ -331,6 +336,10 @@ export default class BranchOverview extends React.PureComponent { ({ analyses }) => { if (this.mounted) { this.setState({ + detectedCIOnLastAnalysis: + analyses.length > 0 + ? analyses[0].detectedCI !== undefined && analyses[0].detectedCI !== NO_CI_DETECTED + : undefined, analyses }); } @@ -388,10 +397,11 @@ export default class BranchOverview extends React.PureComponent { }; render() { - const { branch, component } = this.props; + const { branch, branchesEnabled, component, projectBinding } = this.props; const { analyses, appLeak, + detectedCIOnLastAnalysis, graph, loadingStatus, loadingHistory, @@ -414,7 +424,9 @@ export default class BranchOverview extends React.PureComponent { analyses={analyses} appLeak={appLeak} branch={branch} + branchesEnabled={branchesEnabled} component={component} + detectedCIOnLastAnalysis={detectedCIOnLastAnalysis} graph={graph} loadingHistory={loadingHistory} loadingStatus={loadingStatus} @@ -423,6 +435,7 @@ export default class BranchOverview extends React.PureComponent { metrics={metrics} onGraphChange={this.handleGraphChange} period={period} + projectBinding={projectBinding} projectIsEmpty={projectIsEmpty} qgStatuses={qgStatuses} /> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx index e4a40cc4253..52cf4428bbf 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx @@ -20,12 +20,14 @@ import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; +import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; import { ApplicationPeriod } from '../../../types/application'; import { Branch } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; import { GraphType, MeasureHistory } from '../../../types/project-activity'; import { QualityGateStatus } from '../../../types/quality-gates'; import ActivityPanel from './ActivityPanel'; +import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif'; import { MeasuresPanel } from './MeasuresPanel'; import NoCodeWarning from './NoCodeWarning'; import QualityGatePanel from './QualityGatePanel'; @@ -34,7 +36,9 @@ export interface BranchOverviewRendererProps { analyses?: T.Analysis[]; appLeak?: ApplicationPeriod; branch?: Branch; + branchesEnabled?: boolean; component: T.Component; + detectedCIOnLastAnalysis?: boolean; graph?: GraphType; loadingHistory?: boolean; loadingStatus?: boolean; @@ -43,6 +47,7 @@ export interface BranchOverviewRendererProps { metrics?: T.Metric[]; onGraphChange: (graph: GraphType) => void; period?: T.Period; + projectBinding?: ProjectAlmBindingResponse; projectIsEmpty?: boolean; qgStatuses?: QualityGateStatus[]; } @@ -52,7 +57,9 @@ export function BranchOverviewRenderer(props: BranchOverviewRendererProps) { analyses, appLeak, branch, + branchesEnabled, component, + detectedCIOnLastAnalysis, graph, loadingHistory, loadingStatus, @@ -61,6 +68,7 @@ export function BranchOverviewRenderer(props: BranchOverviewRendererProps) { metrics = [], onGraphChange, period, + projectBinding, projectIsEmpty, qgStatuses } = props; @@ -68,50 +76,58 @@ export function BranchOverviewRenderer(props: BranchOverviewRendererProps) { const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period; return ( -
-
- + <> + +
+
+ - {projectIsEmpty ? ( - - ) : ( -
-
- -
- -
-
- + ) : ( +
+
+ +
- +
+
+ + + +
-
- )} + )} +
-
+ ); } diff --git a/server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx b/server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx new file mode 100644 index 00000000000..c345eba2247 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx @@ -0,0 +1,131 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { Link } from 'react-router'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; +import DismissableAlert from '../../../components/ui/DismissableAlert'; +import { isLoggedIn } from '../../../helpers/users'; +import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; +import { PULL_REQUEST_DECORATION_BINDING_CATEGORY } from '../../settings/components/AdditionalCategoryKeys'; + +export interface FirstAnalysisNextStepsNotifProps { + branchesEnabled?: boolean; + component: T.Component; + currentUser: T.CurrentUser; + detectedCIOnLastAnalysis?: boolean; + projectBinding?: ProjectAlmBindingResponse; +} + +export function FirstAnalysisNextStepsNotif(props: FirstAnalysisNextStepsNotifProps) { + const { + component, + currentUser, + branchesEnabled, + detectedCIOnLastAnalysis, + projectBinding + } = props; + + if (!isLoggedIn(currentUser)) { + return null; + } + + const showConfigurePullRequestDecoNotif = branchesEnabled && projectBinding === undefined; + const showConfigureCINotif = + detectedCIOnLastAnalysis !== undefined ? !detectedCIOnLastAnalysis : false; + + if (!showConfigureCINotif && !showConfigurePullRequestDecoNotif) { + return null; + } + + const showOnlyConfigureCI = showConfigureCINotif && !showConfigurePullRequestDecoNotif; + const showOnlyConfigurePR = showConfigurePullRequestDecoNotif && !showConfigureCINotif; + const showBoth = showConfigureCINotif && showConfigurePullRequestDecoNotif; + const isProjectAdmin = component.configuration?.showSettings; + const tutorialsLink = ( + + {translate('overview.project.next_steps.links.set_up_ci')} + + ); + const projectSettingsLink = ( + + {translate('overview.project.next_steps.links.project_settings')} + + ); + + return ( + + {showOnlyConfigureCI && ( + + )} + + {showOnlyConfigurePR && + (isProjectAdmin ? ( + + ) : ( + translate('overview.project.next_steps.set_up_pr_deco') + ))} + + {showBoth && + (isProjectAdmin ? ( + + ) : ( + + ))} + + ); +} + +export default withCurrentUser(FirstAnalysisNextStepsNotif); diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx index db78cc4ec3b..d7374852d03 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx @@ -32,11 +32,11 @@ import { import { getAllTimeMachineData } from '../../../../api/time-machine'; import { getActivityGraph, saveActivityGraph } from '../../../../components/activity-graph/utils'; import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like'; -import { mockComponent } from '../../../../helpers/testMocks'; +import { mockAnalysis, mockComponent } from '../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../types/component'; import { MetricKey } from '../../../../types/metrics'; import { GraphType } from '../../../../types/project-activity'; -import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH } from '../BranchOverview'; +import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview'; import BranchOverviewRenderer from '../BranchOverviewRenderer'; jest.mock('sonar-ui-common/helpers/dates', () => ({ @@ -146,7 +146,13 @@ jest.mock('../../../../api/projectActivity', () => { const { mockAnalysis } = jest.requireActual('../../../../helpers/testMocks'); return { getProjectActivity: jest.fn().mockResolvedValue({ - analyses: [mockAnalysis(), mockAnalysis(), mockAnalysis(), mockAnalysis(), mockAnalysis()] + analyses: [ + mockAnalysis({ detectedCI: 'Cirrus CI' }), + mockAnalysis(), + mockAnalysis(), + mockAnalysis(), + mockAnalysis() + ] }) }; }); @@ -345,6 +351,22 @@ it("should correctly load a component's history", async () => { ); }); +it.each([ + ['no analysis', [], undefined], + ['1 analysis, no CI data', [mockAnalysis()], false], + ['1 analysis, no CI detected', [mockAnalysis({ detectedCI: NO_CI_DETECTED })], false], + ['1 analysis, CI detected', [mockAnalysis({ detectedCI: 'Cirrus CI' })], true] +])( + "should correctly flag a project that wasn't analyzed using a CI (%s)", + async (_, analyses, expected) => { + (getProjectActivity as jest.Mock).mockResolvedValueOnce({ analyses }); + + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(wrapper.state().detectedCIOnLastAnalysis).toBe(expected); + } +); + it('should correctly handle graph type storage', () => { const wrapper = shallowRender(); expect(getActivityGraph).toBeCalledWith(BRANCH_OVERVIEW_ACTIVITY_GRAPH, 'foo'); diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverviewRenderer-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverviewRenderer-test.tsx index 129ebb5091d..1f0b71a5617 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverviewRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverviewRenderer-test.tsx @@ -25,9 +25,9 @@ import { GraphType } from '../../../../types/project-activity'; import { BranchOverviewRenderer, BranchOverviewRendererProps } from '../BranchOverviewRenderer'; it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - expect(shallowRender({ projectIsEmpty: true })).toMatchSnapshot(); - expect(shallowRender({ loadingHistory: true, loadingStatus: true })).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ projectIsEmpty: true })).toMatchSnapshot('empty project'); + expect(shallowRender({ loadingHistory: true, loadingStatus: true })).toMatchSnapshot('loading'); }); function shallowRender(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/FirstAnalysisNextStepsNotif-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/FirstAnalysisNextStepsNotif-test.tsx new file mode 100644 index 00000000000..21494366a55 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/FirstAnalysisNextStepsNotif-test.tsx @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { mockProjectAlmBindingResponse } from '../../../../helpers/mocks/alm-settings'; +import { mockComponent, mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; +import { + FirstAnalysisNextStepsNotif, + FirstAnalysisNextStepsNotifProps +} from '../FirstAnalysisNextStepsNotif'; + +it('should render correctly', () => { + expect(shallowRender({ currentUser: mockCurrentUser() }).type()).toBeNull(); + expect(shallowRender({ detectedCIOnLastAnalysis: false })).toMatchSnapshot( + 'show prompt to configure CI' + ); + expect( + shallowRender({ + projectBinding: undefined + }) + ).toMatchSnapshot('show prompt to configure PR decoration, regular user'); + expect( + shallowRender({ + component: mockComponent({ configuration: { showSettings: true } }), + projectBinding: undefined + }) + ).toMatchSnapshot('show prompt to configure PR decoration, project admin'); + expect( + shallowRender({ + projectBinding: undefined, + detectedCIOnLastAnalysis: false + }) + ).toMatchSnapshot('show prompt to configure PR decoration + CI, regular user'); + expect( + shallowRender({ + component: mockComponent({ configuration: { showSettings: true } }), + projectBinding: undefined, + detectedCIOnLastAnalysis: false + }) + ).toMatchSnapshot('show prompt to configure PR decoration + CI, project admin'); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverview-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverview-test.tsx.snap index 543ff2c4b99..bedeaafe142 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverview-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverview-test.tsx.snap @@ -6,6 +6,7 @@ exports[`application overview should fetch correctly other branch 1`] = ` Array [ Object { "date": "2017-03-01T09:36:01+0100", + "detectedCI": "Cirrus CI", "events": Array [], "key": "foo", "projectVersion": "1.0", @@ -94,6 +95,7 @@ exports[`application overview should fetch correctly other branch 1`] = ` "tags": Array [], } } + detectedCIOnLastAnalysis={true} graph="coverage" loadingHistory={false} loadingStatus={false} @@ -926,6 +928,7 @@ exports[`application overview should render correctly 1`] = ` Array [ Object { "date": "2017-03-01T09:36:01+0100", + "detectedCI": "Cirrus CI", "events": Array [], "key": "foo", "projectVersion": "1.0", @@ -1014,6 +1017,7 @@ exports[`application overview should render correctly 1`] = ` "tags": Array [], } } + detectedCIOnLastAnalysis={true} graph="coverage" loadingHistory={false} loadingStatus={false} @@ -1846,6 +1850,7 @@ exports[`project overview should render correctly 1`] = ` Array [ Object { "date": "2017-03-01T09:36:01+0100", + "detectedCI": "Cirrus CI", "events": Array [], "key": "foo", "projectVersion": "1.0", @@ -1927,6 +1932,7 @@ exports[`project overview should render correctly 1`] = ` "tags": Array [], } } + detectedCIOnLastAnalysis={true} graph="coverage" loadingHistory={false} loadingStatus={false} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverviewRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverviewRenderer-test.tsx.snap index 551eb624b06..0ceef54903a 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverviewRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverviewRenderer-test.tsx.snap @@ -1,62 +1,47 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` -
+exports[`should render correctly: default 1`] = ` + +
-
+
- -
-
- +
+
+
+ - + + graph="issues" + loading={false} + measuresHistory={Array []} + metrics={Array []} + onGraphChange={[MockFunction]} + /> +
-
+ `; -exports[`should render correctly 2`] = ` -
+exports[`should render correctly: empty project 1`] = ` + +
- - + + + ] + } + /> +
-
+ `; -exports[`should render correctly 3`] = ` -
+exports[`should render correctly: loading 1`] = ` + +
-
+
- -
-
- +
+
+
+ - + + graph="issues" + loading={true} + measuresHistory={Array []} + metrics={Array []} + onGraphChange={[MockFunction]} + /> +
-
+ `; diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap new file mode 100644 index 00000000000..cb8c39ea0b6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: show prompt to configure CI 1`] = ` + + + overview.project.next_steps.links.set_up_ci + , + } + } + /> + +`; + +exports[`should render correctly: show prompt to configure PR decoration + CI, project admin 1`] = ` + + + overview.project.next_steps.links.set_up_ci + , + "link_project_settings": + overview.project.next_steps.links.project_settings + , + } + } + /> + +`; + +exports[`should render correctly: show prompt to configure PR decoration + CI, regular user 1`] = ` + + + overview.project.next_steps.links.set_up_ci + , + } + } + /> + +`; + +exports[`should render correctly: show prompt to configure PR decoration, project admin 1`] = ` + + + overview.project.next_steps.links.project_settings + , + } + } + /> + +`; + +exports[`should render correctly: show prompt to configure PR decoration, regular user 1`] = ` + + overview.project.next_steps.set_up_pr_deco + +`; diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx index 9f1da8a7d94..cd1b6ff4a5a 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; +import { withAppState } from '../../../components/hoc/withAppState'; import { Router, withRouter } from '../../../components/hoc/withRouter'; import { isPullRequest } from '../../../helpers/branch-like'; import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; @@ -31,6 +32,7 @@ const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview')); const PullRequestOverview = lazyLoadComponent(() => import('../pullRequests/PullRequestOverview')); interface Props { + appState: Pick; branchLike?: BranchLike; branchLikes: BranchLike[]; component: T.Component; @@ -46,7 +48,13 @@ export class App extends React.PureComponent { }; render() { - const { branchLike, branchLikes, component, projectBinding } = this.props; + const { + appState: { branchesEnabled }, + branchLike, + branchLikes, + component, + projectBinding + } = this.props; if (this.isPortfolio()) { return null; @@ -61,7 +69,7 @@ export class App extends React.PureComponent { <> - {!component.analysisDate ? ( + {!component.analysisDate && ( { hasAnalyses={this.props.isPending || this.props.isInProgress} projectBinding={projectBinding} /> - ) : ( - + )} + + {component.analysisDate && ( + )} ); } } -export default withRouter(App); +export default withRouter(withAppState(App)); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx index c18e129fd84..bd742f4824e 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { isSonarCloud } from '../../../../helpers/system'; +import { mockAppState } from '../../../../helpers/testMocks'; import BranchOverview from '../../branches/BranchOverview'; import { App } from '../App'; @@ -49,6 +50,12 @@ it('should render BranchOverview', () => { function getWrapper(props = {}) { return shallow( - + ); } diff --git a/server/sonar-web/src/main/js/components/ui/DismissableAlert.css b/server/sonar-web/src/main/js/components/ui/DismissableAlert.css new file mode 100644 index 00000000000..59d057e7e96 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/DismissableAlert.css @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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. + */ + +.dismissable-alert-banner .dismissable-alert-content { + max-width: var(--maxPageWidth); + min-width: var(--minPageWidth); +} + +.dismissable-alert-banner .button-icon { + height: var(--tinyControlHeight); + width: var(--tinyControlHeight); +} diff --git a/server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx b/server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx new file mode 100644 index 00000000000..9c3fa5f8dea --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as classNames from 'classnames'; +import * as React from 'react'; +import { ButtonIcon } from 'sonar-ui-common/components/controls/buttons'; +import ClearIcon from 'sonar-ui-common/components/icons/ClearIcon'; +import { Alert, AlertProps } from 'sonar-ui-common/components/ui/Alert'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { get, save } from 'sonar-ui-common/helpers/storage'; +import './DismissableAlert.css'; + +export interface DismissableAlertProps extends AlertProps { + alertKey: string; + children?: React.ReactNode; + className?: string; +} + +export const DISMISSED_ALERT_STORAGE_KEY = 'sonarqube.dismissed_alert'; + +export default function DismissableAlert(props: DismissableAlertProps) { + const { alertKey, children, className, display = 'banner', variant } = props; + const [show, setShow] = React.useState(false); + + React.useEffect(() => { + if (get(DISMISSED_ALERT_STORAGE_KEY, alertKey) !== 'true') { + setShow(true); + } + }, [alertKey]); + + const hideAlert = () => { + save(DISMISSED_ALERT_STORAGE_KEY, 'true', alertKey); + }; + + return !show ? null : ( + +
+
{children}
+ { + hideAlert(); + setShow(false); + }}> + + +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/DismissableAlert-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/DismissableAlert-test.tsx new file mode 100644 index 00000000000..c371d5a0c19 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/DismissableAlert-test.tsx @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { ButtonIcon } from 'sonar-ui-common/components/controls/buttons'; +import { save } from 'sonar-ui-common/helpers/storage'; +import { click } from 'sonar-ui-common/helpers/testUtils'; +import DismissableAlert, { + DismissableAlertProps, + DISMISSED_ALERT_STORAGE_KEY +} from '../DismissableAlert'; + +jest.mock('sonar-ui-common/helpers/storage', () => ({ + get: jest.fn((_: string, suffix: string) => (suffix === 'bar' ? 'true' : undefined)), + save: jest.fn() +})); + +beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f()); +}); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should render correctly with a non-default display', () => { + expect(shallowRender({ display: 'block' })).toMatchSnapshot(); +}); + +it('should not render if it was dismissed', () => { + expect(shallowRender({ alertKey: 'bar' }).type()).toBeNull(); +}); + +it('should correctly allow dismissing', () => { + const wrapper = shallowRender(); + click(wrapper.find(ButtonIcon)); + expect(save).toBeCalledWith(DISMISSED_ALERT_STORAGE_KEY, 'true', 'foo'); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + +
My content
+
+ ); +} diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap new file mode 100644 index 00000000000..7583076acd6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + +
+
+
+ My content +
+
+ + + +
+
+`; + +exports[`should render correctly with a non-default display 1`] = ` + +
+
+
+ My content +
+
+ + + +
+
+`; diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts index 404c070c28f..bb030129076 100644 --- a/server/sonar-web/src/main/js/types/types.d.ts +++ b/server/sonar-web/src/main/js/types/types.d.ts @@ -46,6 +46,7 @@ declare namespace T { interface BaseAnalysis { buildString?: string; + detectedCI?: string; events: AnalysisEvent[]; key: string; manualNewCodePeriodBaseline?: boolean; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 5e9f4e487c0..7d6cbeeb5b9 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1846,6 +1846,8 @@ alert.tooltip.warning=This is a warning message. alert.tooltip.success=This is a success message. alert.tooltip.info=This is an info message. +alert.dismiss=Dismiss this message + #------------------------------------------------------------------------------ # @@ -2966,6 +2968,13 @@ overview.project.branch_X_empty=The "{0}" branch of this project is empty. overview.project.main_branch_no_lines_of_code=The main branch has no lines of code. overview.project.main_branch_empty=The main branch of this project is empty. overview.project.branch_needs_new_analysis=The branch data is incomplete. Run a new analysis to update it. +overview.project.next_steps.set_up_pr_deco_and_ci.admin=To benefit from more of SonarQube's features, {link_ci} and set up DevOps platform integration in your {link_project_settings}. +overview.project.next_steps.set_up_pr_deco_and_ci=To benefit from more of SonarQube's features, {link_ci} and ask a project administrator to set up DevOps platform integration. +overview.project.next_steps.set_up_pr_deco.admin=To benefit from more of SonarQube's features, set up DevOps platform integration in your {link_project_settings}. +overview.project.next_steps.set_up_pr_deco=To benefit from more of SonarQube's features, ask a project administrator to set up DevOps platform integration. +overview.project.next_steps.set_up_ci=To benefit from more of SonarQube's features, {link}. +overview.project.next_steps.links.project_settings=project settings +overview.project.next_steps.links.set_up_ci=set up analysis in your favorite CI overview.coverage_on=Coverage on overview.coverage_on_X_lines=Coverage on {count} Lines to cover