Browse Source

SONAR-14935 Prompt users for next steps once the first analysis is finished

tags/9.0.0.45539
Wouter Admiraal 2 years ago
parent
commit
ef084e20eb
17 changed files with 1039 additions and 301 deletions
  1. 14
    1
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
  2. 52
    36
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
  3. 131
    0
      server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx
  4. 25
    3
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx
  5. 3
    3
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverviewRenderer-test.tsx
  6. 72
    0
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/FirstAnalysisNextStepsNotif-test.tsx
  7. 6
    0
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverview-test.tsx.snap
  8. 330
    252
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverviewRenderer-test.tsx.snap
  9. 146
    0
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap
  10. 20
    5
      server/sonar-web/src/main/js/apps/overview/components/App.tsx
  11. 8
    1
      server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
  12. 29
    0
      server/sonar-web/src/main/js/components/ui/DismissableAlert.css
  13. 69
    0
      server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx
  14. 65
    0
      server/sonar-web/src/main/js/components/ui/__tests__/DismissableAlert-test.tsx
  15. 59
    0
      server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap
  16. 1
    0
      server/sonar-web/src/main/js/types/types.d.ts
  17. 9
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 14
- 1
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx View File

@@ -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}
/>

+ 52
- 36
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx View File

@@ -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>
</>
);
}


+ 131
- 0
server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx View File

@@ -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);

+ 25
- 3
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx View File

@@ -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');

+ 3
- 3
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverviewRenderer-test.tsx View File

@@ -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> = {}) {

+ 72
- 0
server/sonar-web/src/main/js/apps/overview/branches/__tests__/FirstAnalysisNextStepsNotif-test.tsx View File

@@ -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}
/>
);
}

+ 6
- 0
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverview-test.tsx.snap View File

@@ -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}

+ 330
- 252
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverviewRenderer-test.tsx.snap View File

@@ -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>
`;

+ 146
- 0
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/FirstAnalysisNextStepsNotif-test.tsx.snap View File

@@ -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>
`;

+ 20
- 5
server/sonar-web/src/main/js/apps/overview/components/App.tsx View File

@@ -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));

+ 8
- 1
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx View File

@@ -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}
/>
);
}

+ 29
- 0
server/sonar-web/src/main/js/components/ui/DismissableAlert.css View File

@@ -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);
}

+ 69
- 0
server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx View File

@@ -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>
);
}

+ 65
- 0
server/sonar-web/src/main/js/components/ui/__tests__/DismissableAlert-test.tsx View File

@@ -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>
);
}

+ 59
- 0
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap View File

@@ -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>
`;

+ 1
- 0
server/sonar-web/src/main/js/types/types.d.ts View File

@@ -46,6 +46,7 @@ declare namespace T {

interface BaseAnalysis {
buildString?: string;
detectedCI?: string;
events: AnalysisEvent[];
key: string;
manualNewCodePeriodBaseline?: boolean;

+ 9
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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

Loading…
Cancel
Save