diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2021-06-11 10:33:13 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-06-17 20:03:07 +0000 |
commit | ef084e20eb019c2a8f37b4e7827c5307bc1e2e5d (patch) | |
tree | cc7a088d051276d6a68fd9be8ce8dfbe9072bab9 | |
parent | 96d6d7532cb485deb8b95865fc43144b33e2f202 (diff) | |
download | sonarqube-ef084e20eb019c2a8f37b4e7827c5307bc1e2e5d.tar.gz sonarqube-ef084e20eb019c2a8f37b4e7827c5307bc1e2e5d.zip |
SONAR-14935 Prompt users for next steps once the first analysis is finished
17 files changed, 1039 insertions, 301 deletions
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<Props, State> { ({ 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<Props, State> { }; 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<Props, State> { 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<Props, State> { 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 ( - <div className="page page-limited"> - <div className="overview"> - <A11ySkipTarget anchor="overview_main" /> + <> + <FirstAnalysisNextStepsNotif + component={component} + branchesEnabled={branchesEnabled} + detectedCIOnLastAnalysis={detectedCIOnLastAnalysis} + projectBinding={projectBinding} + /> + <div className="page page-limited"> + <div className="overview"> + <A11ySkipTarget anchor="overview_main" /> - {projectIsEmpty ? ( - <NoCodeWarning branchLike={branch} component={component} measures={measures} /> - ) : ( - <div className="display-flex-row"> - <div className="width-25 big-spacer-right"> - <QualityGatePanel - component={component} - loading={loadingStatus} - qgStatuses={qgStatuses} - /> - </div> - - <div className="flex-1"> - <div className="display-flex-column"> - <MeasuresPanel - appLeak={appLeak} - branch={branch} + {projectIsEmpty ? ( + <NoCodeWarning branchLike={branch} component={component} measures={measures} /> + ) : ( + <div className="display-flex-row"> + <div className="width-25 big-spacer-right"> + <QualityGatePanel component={component} loading={loadingStatus} - measures={measures} - period={period} + qgStatuses={qgStatuses} /> + </div> - <ActivityPanel - analyses={analyses} - branchLike={branch} - component={component} - graph={graph} - leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)} - loading={loadingHistory} - measuresHistory={measuresHistory} - metrics={metrics} - onGraphChange={onGraphChange} - /> + <div className="flex-1"> + <div className="display-flex-column"> + <MeasuresPanel + appLeak={appLeak} + branch={branch} + component={component} + loading={loadingStatus} + measures={measures} + period={period} + /> + + <ActivityPanel + analyses={analyses} + branchLike={branch} + component={component} + graph={graph} + leakPeriodDate={leakPeriod && parseDate(leakPeriod.date)} + loading={loadingHistory} + measuresHistory={measuresHistory} + metrics={metrics} + onGraphChange={onGraphChange} + /> + </div> </div> </div> - </div> - )} + )} + </div> </div> - </div> + </> ); } 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 = ( + <Link + to={{ + pathname: '/tutorials', + query: { id: component.key } + }}> + {translate('overview.project.next_steps.links.set_up_ci')} + </Link> + ); + const projectSettingsLink = ( + <Link + to={{ + pathname: '/project/settings', + query: { + id: component.key, + category: PULL_REQUEST_DECORATION_BINDING_CATEGORY + } + }}> + {translate('overview.project.next_steps.links.project_settings')} + </Link> + ); + + return ( + <DismissableAlert alertKey={`config_ci_pr_deco.${component.key}`} variant="info"> + {showOnlyConfigureCI && ( + <FormattedMessage + defaultMessage={translate('overview.project.next_steps.set_up_ci')} + id="overview.project.next_steps.set_up_ci" + values={{ + link: tutorialsLink + }} + /> + )} + + {showOnlyConfigurePR && + (isProjectAdmin ? ( + <FormattedMessage + defaultMessage={translate('overview.project.next_steps.set_up_pr_deco.admin')} + id="overview.project.next_steps.set_up_pr_deco.admin" + values={{ + link_project_settings: projectSettingsLink + }} + /> + ) : ( + translate('overview.project.next_steps.set_up_pr_deco') + ))} + + {showBoth && + (isProjectAdmin ? ( + <FormattedMessage + defaultMessage={translate('overview.project.next_steps.set_up_pr_deco_and_ci.admin')} + id="overview.project.next_steps.set_up_pr_deco_and_ci.admin" + values={{ + link_ci: tutorialsLink, + link_project_settings: projectSettingsLink + }} + /> + ) : ( + <FormattedMessage + defaultMessage={translate('overview.project.next_steps.set_up_pr_deco_and_ci')} + id="overview.project.next_steps.set_up_pr_deco_and_ci" + values={{ link_ci: tutorialsLink }} + /> + ))} + </DismissableAlert> + ); +} + +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<BranchOverviewRendererProps> = {}) { 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<FirstAnalysisNextStepsNotifProps> = {}) { + return shallow<FirstAnalysisNextStepsNotifProps>( + <FirstAnalysisNextStepsNotif + component={mockComponent()} + branchesEnabled={true} + currentUser={mockLoggedInUser()} + detectedCIOnLastAnalysis={true} + projectBinding={mockProjectAlmBindingResponse()} + {...props} + /> + ); +} 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`] = ` -<div - className="page page-limited" -> +exports[`should render correctly: default 1`] = ` +<Fragment> + <Connect(withCurrentUser(FirstAnalysisNextStepsNotif)) + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> <div - className="overview" + className="page page-limited" > - <A11ySkipTarget - anchor="overview_main" - /> <div - className="display-flex-row" + className="overview" > + <A11ySkipTarget + anchor="overview_main" + /> <div - className="width-25 big-spacer-right" - > - <Memo(QualityGatePanel) - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": Array [], - } - } - loading={false} - /> - </div> - <div - className="flex-1" + className="display-flex-row" > <div - className="display-flex-column" + className="width-25 big-spacer-right" > - <MeasuresPanel - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } + <Memo(QualityGatePanel) component={ Object { "breadcrumbs": Array [], @@ -80,194 +65,246 @@ exports[`should render correctly 1`] = ` } } loading={false} - measures={ - Array [ + /> + </div> + <div + className="flex-1" + > + <div + className="display-flex-column" + > + <MeasuresPanel + branch={ Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", }, - "period": Object { + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + loading={false} + measures={ + Array [ + Object { "bestValue": true, - "index": 1, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, "value": "1.0", }, - "value": "1.0", - }, - ] - } - /> - <Memo(ActivityPanel) - branchLike={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", + ] } - } - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", + /> + <Memo(ActivityPanel) + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", "name": "Sonar way", }, - ], - "tags": Array [], + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } } - } - graph="issues" - loading={false} - measuresHistory={Array []} - metrics={Array []} - onGraphChange={[MockFunction]} - /> + graph="issues" + loading={false} + measuresHistory={Array []} + metrics={Array []} + onGraphChange={[MockFunction]} + /> + </div> </div> </div> </div> </div> -</div> +</Fragment> `; -exports[`should render correctly 2`] = ` -<div - className="page page-limited" -> +exports[`should render correctly: empty project 1`] = ` +<Fragment> + <Connect(withCurrentUser(FirstAnalysisNextStepsNotif)) + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> <div - className="overview" + className="page page-limited" > - <A11ySkipTarget - anchor="overview_main" - /> - <Memo(NoCodeWarning) - branchLike={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", + <div + className="overview" + > + <A11ySkipTarget + anchor="overview_main" + /> + <Memo(NoCodeWarning) + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } } - } - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", "name": "Sonar way", }, - ], - "tags": Array [], + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } } - } - measures={ - Array [ - Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", - }, - "period": Object { + measures={ + Array [ + Object { "bestValue": true, - "index": 1, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, "value": "1.0", }, - "value": "1.0", - }, - ] - } - /> + ] + } + /> + </div> </div> -</div> +</Fragment> `; -exports[`should render correctly 3`] = ` -<div - className="page page-limited" -> +exports[`should render correctly: loading 1`] = ` +<Fragment> + <Connect(withCurrentUser(FirstAnalysisNextStepsNotif)) + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + /> <div - className="overview" + className="page page-limited" > - <A11ySkipTarget - anchor="overview_main" - /> <div - className="display-flex-row" + className="overview" > + <A11ySkipTarget + anchor="overview_main" + /> <div - className="width-25 big-spacer-right" - > - <Memo(QualityGatePanel) - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": Array [], - } - } - loading={true} - /> - </div> - <div - className="flex-1" + className="display-flex-row" > <div - className="display-flex-column" + className="width-25 big-spacer-right" > - <MeasuresPanel - branch={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - } - } + <Memo(QualityGatePanel) component={ Object { "breadcrumbs": Array [], @@ -291,67 +328,108 @@ exports[`should render correctly 3`] = ` } } loading={true} - measures={ - Array [ + /> + </div> + <div + className="flex-1" + > + <div + className="display-flex-column" + > + <MeasuresPanel + branch={ Object { - "bestValue": true, - "leak": "1", - "metric": Object { - "id": "coverage", - "key": "coverage", - "name": "Coverage", - "type": "PERCENT", + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", }, - "period": Object { + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + loading={true} + measures={ + Array [ + Object { "bestValue": true, - "index": 1, + "leak": "1", + "metric": Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "period": Object { + "bestValue": true, + "index": 1, + "value": "1.0", + }, "value": "1.0", }, - "value": "1.0", - }, - ] - } - /> - <Memo(ActivityPanel) - branchLike={ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", + ] } - } - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", + /> + <Memo(ActivityPanel) + branchLike={ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + } + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", "name": "Sonar way", }, - ], - "tags": Array [], + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } } - } - graph="issues" - loading={true} - measuresHistory={Array []} - metrics={Array []} - onGraphChange={[MockFunction]} - /> + graph="issues" + loading={true} + measuresHistory={Array []} + metrics={Array []} + onGraphChange={[MockFunction]} + /> + </div> </div> </div> </div> </div> -</div> +</Fragment> `; 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`] = ` +<DismissableAlert + alertKey="config_ci_pr_deco.my-project" + variant="info" +> + <FormattedMessage + defaultMessage="overview.project.next_steps.set_up_ci" + id="overview.project.next_steps.set_up_ci" + values={ + Object { + "link": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/tutorials", + "query": Object { + "id": "my-project", + }, + } + } + > + overview.project.next_steps.links.set_up_ci + </Link>, + } + } + /> +</DismissableAlert> +`; + +exports[`should render correctly: show prompt to configure PR decoration + CI, project admin 1`] = ` +<DismissableAlert + alertKey="config_ci_pr_deco.my-project" + variant="info" +> + <FormattedMessage + defaultMessage="overview.project.next_steps.set_up_pr_deco_and_ci.admin" + id="overview.project.next_steps.set_up_pr_deco_and_ci.admin" + values={ + Object { + "link_ci": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/tutorials", + "query": Object { + "id": "my-project", + }, + } + } + > + overview.project.next_steps.links.set_up_ci + </Link>, + "link_project_settings": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "category": "pull_request_decoration_binding", + "id": "my-project", + }, + } + } + > + overview.project.next_steps.links.project_settings + </Link>, + } + } + /> +</DismissableAlert> +`; + +exports[`should render correctly: show prompt to configure PR decoration + CI, regular user 1`] = ` +<DismissableAlert + alertKey="config_ci_pr_deco.my-project" + variant="info" +> + <FormattedMessage + defaultMessage="overview.project.next_steps.set_up_pr_deco_and_ci" + id="overview.project.next_steps.set_up_pr_deco_and_ci" + values={ + Object { + "link_ci": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/tutorials", + "query": Object { + "id": "my-project", + }, + } + } + > + overview.project.next_steps.links.set_up_ci + </Link>, + } + } + /> +</DismissableAlert> +`; + +exports[`should render correctly: show prompt to configure PR decoration, project admin 1`] = ` +<DismissableAlert + alertKey="config_ci_pr_deco.my-project" + variant="info" +> + <FormattedMessage + defaultMessage="overview.project.next_steps.set_up_pr_deco.admin" + id="overview.project.next_steps.set_up_pr_deco.admin" + values={ + Object { + "link_project_settings": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "category": "pull_request_decoration_binding", + "id": "my-project", + }, + } + } + > + overview.project.next_steps.links.project_settings + </Link>, + } + } + /> +</DismissableAlert> +`; + +exports[`should render correctly: show prompt to configure PR decoration, regular user 1`] = ` +<DismissableAlert + alertKey="config_ci_pr_deco.my-project" + variant="info" +> + overview.project.next_steps.set_up_pr_deco +</DismissableAlert> +`; 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<T.AppState, 'branchesEnabled'>; branchLike?: BranchLike; branchLikes: BranchLike[]; component: T.Component; @@ -46,7 +48,13 @@ export class App extends React.PureComponent<Props> { }; 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<Props> { <> <Suggestions suggestions="overview" /> - {!component.analysisDate ? ( + {!component.analysisDate && ( <EmptyOverview branchLike={branchLike} branchLikes={branchLikes} @@ -69,12 +77,19 @@ export class App extends React.PureComponent<Props> { hasAnalyses={this.props.isPending || this.props.isInProgress} projectBinding={projectBinding} /> - ) : ( - <BranchOverview branch={branchLike} component={component} /> + )} + + {component.analysisDate && ( + <BranchOverview + branch={branchLike} + branchesEnabled={branchesEnabled} + component={component} + projectBinding={projectBinding} + /> )} </> ); } } -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( - <App branchLikes={[]} component={component} router={{ replace: jest.fn() }} {...props} /> + <App + appState={mockAppState()} + branchLikes={[]} + component={component} + router={{ replace: jest.fn() }} + {...props} + /> ); } 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 : ( + <Alert + className={classNames(`dismissable-alert-${display}`, className)} + display={display} + variant={variant}> + <div className="display-flex-center dismissable-alert-content"> + <div className="flex-1">{children}</div> + <ButtonIcon + aria-label={translate('alert.dismiss')} + onClick={() => { + hideAlert(); + setShow(false); + }}> + <ClearIcon size={12} thin={true} /> + </ButtonIcon> + </div> + </Alert> + ); +} 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<DismissableAlertProps> = {}) { + return shallow<DismissableAlertProps>( + <DismissableAlert alertKey="foo" variant="info" {...props}> + <div>My content</div> + </DismissableAlert> + ); +} 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`] = ` +<Alert + className="dismissable-alert-banner" + display="banner" + variant="info" +> + <div + className="display-flex-center dismissable-alert-content" + > + <div + className="flex-1" + > + <div> + My content + </div> + </div> + <ButtonIcon + aria-label="alert.dismiss" + onClick={[Function]} + > + <ClearIcon + size={12} + thin={true} + /> + </ButtonIcon> + </div> +</Alert> +`; + +exports[`should render correctly with a non-default display 1`] = ` +<Alert + className="dismissable-alert-block" + display="block" + variant="info" +> + <div + className="display-flex-center dismissable-alert-content" + > + <div + className="flex-1" + > + <div> + My content + </div> + </div> + <ButtonIcon + aria-label="alert.dismiss" + onClick={[Function]} + > + <ClearIcon + size={12} + thin={true} + /> + </ButtonIcon> + </div> +</Alert> +`; 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 |