@@ -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} | |||
/> |
@@ -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> | |||
</> | |||
); | |||
} | |||
@@ -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); |
@@ -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'); |
@@ -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> = {}) { |
@@ -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,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} |
@@ -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> | |||
`; |
@@ -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,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)); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -46,6 +46,7 @@ declare namespace T { | |||
interface BaseAnalysis { | |||
buildString?: string; | |||
detectedCI?: string; | |||
events: AnalysisEvent[]; | |||
key: string; | |||
manualNewCodePeriodBaseline?: boolean; |
@@ -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 |