Browse Source

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

tags/9.0.0.45539
Wouter Admiraal 3 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

extractStatusConditionsFromApplicationStatusChildProject, extractStatusConditionsFromApplicationStatusChildProject,
extractStatusConditionsFromProjectStatus extractStatusConditionsFromProjectStatus
} from '../../../helpers/qualityGates'; } from '../../../helpers/qualityGates';
import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
import { ApplicationPeriod } from '../../../types/application'; import { ApplicationPeriod } from '../../../types/application';
import { Branch, BranchLike } from '../../../types/branch-like'; import { Branch, BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component'; import { ComponentQualifier } from '../../../types/component';


interface Props { interface Props {
branch?: Branch; branch?: Branch;
branchesEnabled?: boolean;
component: T.Component; component: T.Component;
projectBinding?: ProjectAlmBindingResponse;
} }


interface State { interface State {
analyses?: T.Analysis[]; analyses?: T.Analysis[];
appLeak?: ApplicationPeriod; appLeak?: ApplicationPeriod;
detectedCIOnLastAnalysis?: boolean;
graph: GraphType; graph: GraphType;
loadingHistory?: boolean; loadingHistory?: boolean;
loadingStatus?: boolean; loadingStatus?: boolean;
} }


export const BRANCH_OVERVIEW_ACTIVITY_GRAPH = 'sonar_branch_overview.graph'; export const BRANCH_OVERVIEW_ACTIVITY_GRAPH = 'sonar_branch_overview.graph';
export const NO_CI_DETECTED = 'undetected';


// Get all history data over the past year. // Get all history data over the past year.
const FROM_DATE = toNotSoISOString(new Date().setFullYear(new Date().getFullYear() - 1)); const FROM_DATE = toNotSoISOString(new Date().setFullYear(new Date().getFullYear() - 1));
({ analyses }) => { ({ analyses }) => {
if (this.mounted) { if (this.mounted) {
this.setState({ this.setState({
detectedCIOnLastAnalysis:
analyses.length > 0
? analyses[0].detectedCI !== undefined && analyses[0].detectedCI !== NO_CI_DETECTED
: undefined,
analyses analyses
}); });
} }
}; };


render() { render() {
const { branch, component } = this.props;
const { branch, branchesEnabled, component, projectBinding } = this.props;
const { const {
analyses, analyses,
appLeak, appLeak,
detectedCIOnLastAnalysis,
graph, graph,
loadingStatus, loadingStatus,
loadingHistory, loadingHistory,
analyses={analyses} analyses={analyses}
appLeak={appLeak} appLeak={appLeak}
branch={branch} branch={branch}
branchesEnabled={branchesEnabled}
component={component} component={component}
detectedCIOnLastAnalysis={detectedCIOnLastAnalysis}
graph={graph} graph={graph}
loadingHistory={loadingHistory} loadingHistory={loadingHistory}
loadingStatus={loadingStatus} loadingStatus={loadingStatus}
metrics={metrics} metrics={metrics}
onGraphChange={this.handleGraphChange} onGraphChange={this.handleGraphChange}
period={period} period={period}
projectBinding={projectBinding}
projectIsEmpty={projectIsEmpty} projectIsEmpty={projectIsEmpty}
qgStatuses={qgStatuses} qgStatuses={qgStatuses}
/> />

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

import * as React from 'react'; import * as React from 'react';
import { parseDate } from 'sonar-ui-common/helpers/dates'; import { parseDate } from 'sonar-ui-common/helpers/dates';
import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
import { ApplicationPeriod } from '../../../types/application'; import { ApplicationPeriod } from '../../../types/application';
import { Branch } from '../../../types/branch-like'; import { Branch } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component'; import { ComponentQualifier } from '../../../types/component';
import { GraphType, MeasureHistory } from '../../../types/project-activity'; import { GraphType, MeasureHistory } from '../../../types/project-activity';
import { QualityGateStatus } from '../../../types/quality-gates'; import { QualityGateStatus } from '../../../types/quality-gates';
import ActivityPanel from './ActivityPanel'; import ActivityPanel from './ActivityPanel';
import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
import { MeasuresPanel } from './MeasuresPanel'; import { MeasuresPanel } from './MeasuresPanel';
import NoCodeWarning from './NoCodeWarning'; import NoCodeWarning from './NoCodeWarning';
import QualityGatePanel from './QualityGatePanel'; import QualityGatePanel from './QualityGatePanel';
analyses?: T.Analysis[]; analyses?: T.Analysis[];
appLeak?: ApplicationPeriod; appLeak?: ApplicationPeriod;
branch?: Branch; branch?: Branch;
branchesEnabled?: boolean;
component: T.Component; component: T.Component;
detectedCIOnLastAnalysis?: boolean;
graph?: GraphType; graph?: GraphType;
loadingHistory?: boolean; loadingHistory?: boolean;
loadingStatus?: boolean; loadingStatus?: boolean;
metrics?: T.Metric[]; metrics?: T.Metric[];
onGraphChange: (graph: GraphType) => void; onGraphChange: (graph: GraphType) => void;
period?: T.Period; period?: T.Period;
projectBinding?: ProjectAlmBindingResponse;
projectIsEmpty?: boolean; projectIsEmpty?: boolean;
qgStatuses?: QualityGateStatus[]; qgStatuses?: QualityGateStatus[];
} }
analyses, analyses,
appLeak, appLeak,
branch, branch,
branchesEnabled,
component, component,
detectedCIOnLastAnalysis,
graph, graph,
loadingHistory, loadingHistory,
loadingStatus, loadingStatus,
metrics = [], metrics = [],
onGraphChange, onGraphChange,
period, period,
projectBinding,
projectIsEmpty, projectIsEmpty,
qgStatuses qgStatuses
} = props; } = props;
const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period; const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period;


return ( 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} component={component}
loading={loadingStatus} 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>
</div> </div>
</div>
</>
); );
} }



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

/*
* 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

import { getAllTimeMachineData } from '../../../../api/time-machine'; import { getAllTimeMachineData } from '../../../../api/time-machine';
import { getActivityGraph, saveActivityGraph } from '../../../../components/activity-graph/utils'; import { getActivityGraph, saveActivityGraph } from '../../../../components/activity-graph/utils';
import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like'; 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 { ComponentQualifier } from '../../../../types/component';
import { MetricKey } from '../../../../types/metrics'; import { MetricKey } from '../../../../types/metrics';
import { GraphType } from '../../../../types/project-activity'; 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'; import BranchOverviewRenderer from '../BranchOverviewRenderer';


jest.mock('sonar-ui-common/helpers/dates', () => ({ jest.mock('sonar-ui-common/helpers/dates', () => ({
const { mockAnalysis } = jest.requireActual('../../../../helpers/testMocks'); const { mockAnalysis } = jest.requireActual('../../../../helpers/testMocks');
return { return {
getProjectActivity: jest.fn().mockResolvedValue({ getProjectActivity: jest.fn().mockResolvedValue({
analyses: [mockAnalysis(), mockAnalysis(), mockAnalysis(), mockAnalysis(), mockAnalysis()]
analyses: [
mockAnalysis({ detectedCI: 'Cirrus CI' }),
mockAnalysis(),
mockAnalysis(),
mockAnalysis(),
mockAnalysis()
]
}) })
}; };
}); });
); );
}); });


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', () => { it('should correctly handle graph type storage', () => {
const wrapper = shallowRender(); const wrapper = shallowRender();
expect(getActivityGraph).toBeCalledWith(BRANCH_OVERVIEW_ACTIVITY_GRAPH, 'foo'); 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

import { BranchOverviewRenderer, BranchOverviewRendererProps } from '../BranchOverviewRenderer'; import { BranchOverviewRenderer, BranchOverviewRendererProps } from '../BranchOverviewRenderer';


it('should render correctly', () => { 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> = {}) { function shallowRender(props: Partial<BranchOverviewRendererProps> = {}) {

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

/*
* 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

Array [ Array [
Object { Object {
"date": "2017-03-01T09:36:01+0100", "date": "2017-03-01T09:36:01+0100",
"detectedCI": "Cirrus CI",
"events": Array [], "events": Array [],
"key": "foo", "key": "foo",
"projectVersion": "1.0", "projectVersion": "1.0",
"tags": Array [], "tags": Array [],
} }
} }
detectedCIOnLastAnalysis={true}
graph="coverage" graph="coverage"
loadingHistory={false} loadingHistory={false}
loadingStatus={false} loadingStatus={false}
Array [ Array [
Object { Object {
"date": "2017-03-01T09:36:01+0100", "date": "2017-03-01T09:36:01+0100",
"detectedCI": "Cirrus CI",
"events": Array [], "events": Array [],
"key": "foo", "key": "foo",
"projectVersion": "1.0", "projectVersion": "1.0",
"tags": Array [], "tags": Array [],
} }
} }
detectedCIOnLastAnalysis={true}
graph="coverage" graph="coverage"
loadingHistory={false} loadingHistory={false}
loadingStatus={false} loadingStatus={false}
Array [ Array [
Object { Object {
"date": "2017-03-01T09:36:01+0100", "date": "2017-03-01T09:36:01+0100",
"detectedCI": "Cirrus CI",
"events": Array [], "events": Array [],
"key": "foo", "key": "foo",
"projectVersion": "1.0", "projectVersion": "1.0",
"tags": Array [], "tags": Array [],
} }
} }
detectedCIOnLastAnalysis={true}
graph="coverage" graph="coverage"
loadingHistory={false} loadingHistory={false}
loadingStatus={false} loadingStatus={false}

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

// Jest Snapshot v1, https://goo.gl/fbAQLP // 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 <div
className="overview"
className="page page-limited"
> >
<A11ySkipTarget
anchor="overview_main"
/>
<div <div
className="display-flex-row"
className="overview"
> >
<A11ySkipTarget
anchor="overview_main"
/>
<div <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 <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={ component={
Object { Object {
"breadcrumbs": Array [], "breadcrumbs": Array [],
} }
} }
loading={false} loading={false}
measures={
Array [
/>
</div>
<div
className="flex-1"
>
<div
className="display-flex-column"
>
<MeasuresPanel
branch={
Object { 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, "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",
}, },
"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", "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> </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 <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", "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, "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",
}, },
"value": "1.0",
},
]
}
/>
]
}
/>
</div>
</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 <div
className="overview"
className="page page-limited"
> >
<A11ySkipTarget
anchor="overview_main"
/>
<div <div
className="display-flex-row"
className="overview"
> >
<A11ySkipTarget
anchor="overview_main"
/>
<div <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 <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={ component={
Object { Object {
"breadcrumbs": Array [], "breadcrumbs": Array [],
} }
} }
loading={true} loading={true}
measures={
Array [
/>
</div>
<div
className="flex-1"
>
<div
className="display-flex-column"
>
<MeasuresPanel
branch={
Object { 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, "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",
}, },
"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", "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> </div>
</div> </div>
</div>
</Fragment>
`; `;

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

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

import * as React from 'react'; import * as React from 'react';
import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent'; import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { withAppState } from '../../../components/hoc/withAppState';
import { Router, withRouter } from '../../../components/hoc/withRouter'; import { Router, withRouter } from '../../../components/hoc/withRouter';
import { isPullRequest } from '../../../helpers/branch-like'; import { isPullRequest } from '../../../helpers/branch-like';
import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
const PullRequestOverview = lazyLoadComponent(() => import('../pullRequests/PullRequestOverview')); const PullRequestOverview = lazyLoadComponent(() => import('../pullRequests/PullRequestOverview'));


interface Props { interface Props {
appState: Pick<T.AppState, 'branchesEnabled'>;
branchLike?: BranchLike; branchLike?: BranchLike;
branchLikes: BranchLike[]; branchLikes: BranchLike[];
component: T.Component; component: T.Component;
}; };


render() { render() {
const { branchLike, branchLikes, component, projectBinding } = this.props;
const {
appState: { branchesEnabled },
branchLike,
branchLikes,
component,
projectBinding
} = this.props;


if (this.isPortfolio()) { if (this.isPortfolio()) {
return null; return null;
<> <>
<Suggestions suggestions="overview" /> <Suggestions suggestions="overview" />


{!component.analysisDate ? (
{!component.analysisDate && (
<EmptyOverview <EmptyOverview
branchLike={branchLike} branchLike={branchLike}
branchLikes={branchLikes} branchLikes={branchLikes}
hasAnalyses={this.props.isPending || this.props.isInProgress} hasAnalyses={this.props.isPending || this.props.isInProgress}
projectBinding={projectBinding} 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

import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import { isSonarCloud } from '../../../../helpers/system'; import { isSonarCloud } from '../../../../helpers/system';
import { mockAppState } from '../../../../helpers/testMocks';
import BranchOverview from '../../branches/BranchOverview'; import BranchOverview from '../../branches/BranchOverview';
import { App } from '../App'; import { App } from '../App';




function getWrapper(props = {}) { function getWrapper(props = {}) {
return shallow( 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

/*
* 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

/*
* 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

/*
* 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

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



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

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

alert.tooltip.success=This is a success message. alert.tooltip.success=This is a success message.
alert.tooltip.info=This is an info message. alert.tooltip.info=This is an info message.


alert.dismiss=Dismiss this message



#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# #
overview.project.main_branch_no_lines_of_code=The main branch has no lines of code. 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.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.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=Coverage on
overview.coverage_on_X_lines=Coverage on {count} Lines to cover overview.coverage_on_X_lines=Coverage on {count} Lines to cover

Loading…
Cancel
Save