@@ -52,7 +52,7 @@ export interface ActivityPanelProps { | |||
onGraphChange: (graph: GraphType) => void; | |||
} | |||
const MAX_ANALYSES_NB = 5; | |||
export const MAX_ANALYSES_NB = 5; | |||
const MAX_GRAPH_NB = 2; | |||
const MAX_SERIES_PER_GRAPH = 3; | |||
@@ -104,6 +104,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp | |||
<div className="sw-flex-1"> | |||
<div className="sw-flex sw-flex-col sw-pt-6"> | |||
<MeasuresPanel | |||
analyses={analyses} | |||
appLeak={appLeak} | |||
branch={branch} | |||
component={component} |
@@ -42,7 +42,7 @@ export function Event({ event }: Props) { | |||
if (event.category === ProjectAnalysisEventCategory.SqUpgrade) { | |||
return ( | |||
<FlagMessage className="sw-my-1" variant="info"> | |||
<FlagMessage className="sw-my-1" id={event.key} variant="info"> | |||
<FormattedMessage | |||
id="event.sqUpgrade" | |||
values={{ |
@@ -18,7 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { isBefore, sub } from 'date-fns'; | |||
import { | |||
ButtonLink, | |||
Card, | |||
CoverageIndicator, | |||
DuplicationsIndicator, | |||
@@ -29,6 +31,7 @@ import { | |||
ToggleButton, | |||
} from 'design-system'; | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
import ComponentReportActions from '../../../components/controls/ComponentReportActions'; | |||
import { Location, withRouter } from '../../../components/hoc/withRouter'; | |||
@@ -41,15 +44,18 @@ import { Branch } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { IssueType } from '../../../types/issues'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { Analysis, ProjectAnalysisEventCategory } from '../../../types/project-activity'; | |||
import { QualityGateStatus } from '../../../types/quality-gates'; | |||
import { Component, MeasureEnhanced, Period } from '../../../types/types'; | |||
import { MeasurementType, parseQuery } from '../utils'; | |||
import { MAX_ANALYSES_NB } from './ActivityPanel'; | |||
import { LeakPeriodInfo } from './LeakPeriodInfo'; | |||
import MeasuresPanelIssueMeasure from './MeasuresPanelIssueMeasure'; | |||
import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode'; | |||
import MeasuresPanelPercentMeasure from './MeasuresPanelPercentMeasure'; | |||
export interface MeasuresPanelProps { | |||
analyses?: Analysis[]; | |||
appLeak?: ApplicationPeriod; | |||
branch?: Branch; | |||
component: Component; | |||
@@ -65,8 +71,11 @@ export enum MeasuresPanelTabs { | |||
Overall = 'overall', | |||
} | |||
const SQ_UPGRADE_NOTIFICATION_TIMEOUT = { weeks: 3 }; | |||
export function MeasuresPanel(props: MeasuresPanelProps) { | |||
const { | |||
analyses, | |||
appLeak, | |||
branch, | |||
component, | |||
@@ -94,6 +103,43 @@ export function MeasuresPanel(props: MeasuresPanelProps) { | |||
const isNewCodeTab = tab === MeasuresPanelTabs.New; | |||
const recentSqUpgradeEvent = React.useMemo(() => { | |||
if (!analyses || analyses.length === 0) { | |||
return undefined; | |||
} | |||
const notificationExpirationTime = sub(new Date(), SQ_UPGRADE_NOTIFICATION_TIMEOUT); | |||
for (const analysis of analyses.slice(0, MAX_ANALYSES_NB)) { | |||
if (isBefore(new Date(analysis.date), notificationExpirationTime)) { | |||
return undefined; | |||
} | |||
let sqUpgradeEvent = undefined; | |||
let hasQpUpdateEvent = false; | |||
for (const event of analysis.events) { | |||
sqUpgradeEvent = | |||
event.category === ProjectAnalysisEventCategory.SqUpgrade ? event : sqUpgradeEvent; | |||
hasQpUpdateEvent = | |||
hasQpUpdateEvent || event.category === ProjectAnalysisEventCategory.QualityProfile; | |||
if (sqUpgradeEvent !== undefined && hasQpUpdateEvent) { | |||
return sqUpgradeEvent; | |||
} | |||
} | |||
} | |||
return undefined; | |||
}, [analyses]); | |||
const scrollToLatestSqUpgradeEvent = () => { | |||
document.querySelector(`#${recentSqUpgradeEvent?.key}`)?.scrollIntoView({ | |||
behavior: 'smooth', | |||
block: 'center', | |||
inline: 'center', | |||
}); | |||
}; | |||
React.useEffect(() => { | |||
// Open Overall tab by default if there are no new measures. | |||
if (loading === false && !hasDiffMeasures && isNewCodeTab) { | |||
@@ -132,6 +178,23 @@ export function MeasuresPanel(props: MeasuresPanelProps) { | |||
</div> | |||
) : ( | |||
<> | |||
{recentSqUpgradeEvent && ( | |||
<div> | |||
<FlagMessage className="sw-mb-4" variant="info"> | |||
<FormattedMessage | |||
id="overview.quality_profiles_update_after_sq_upgrade.message" | |||
values={{ | |||
link: ( | |||
<ButtonLink className="sw-ml-1" onClick={scrollToLatestSqUpgradeEvent}> | |||
<FormattedMessage id="overview.quality_profiles_update_after_sq_upgrade.link" /> | |||
</ButtonLink> | |||
), | |||
sqVersion: recentSqUpgradeEvent.name, | |||
}} | |||
/> | |||
</FlagMessage> | |||
</div> | |||
)} | |||
<div className="sw-flex sw-items-center"> | |||
<ToggleButton onChange={(key) => selectTab(key)} options={tabs} value={tab} /> | |||
{failingConditions > 0 && ( |
@@ -31,7 +31,7 @@ import { getActivityGraph, saveActivityGraph } from '../../../../components/acti | |||
import { isDiffMetric } from '../../../../helpers/measures'; | |||
import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockAnalysis } from '../../../../helpers/mocks/project-activity'; | |||
import { mockAnalysis, mockAnalysisEvent } from '../../../../helpers/mocks/project-activity'; | |||
import { | |||
mockQualityGateApplicationStatus, | |||
mockQualityGateProjectStatus, | |||
@@ -40,8 +40,12 @@ import { mockLoggedInUser, mockPeriod } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { MetricKey, MetricType } from '../../../../types/metrics'; | |||
import { GraphType } from '../../../../types/project-activity'; | |||
import { CaycStatus, Measure, Metric } from '../../../../types/types'; | |||
import { | |||
Analysis, | |||
GraphType, | |||
ProjectAnalysisEventCategory, | |||
} from '../../../../types/project-activity'; | |||
import { CaycStatus, Measure, Metric, Paging } from '../../../../types/types'; | |||
import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview'; | |||
jest.mock('../../../../api/measures', () => { | |||
@@ -402,6 +406,95 @@ it.each([ | |||
}, | |||
); | |||
it.each([ | |||
[ | |||
'no upgrade event', | |||
[ | |||
mockAnalysis({ | |||
events: [mockAnalysisEvent({ category: ProjectAnalysisEventCategory.Other })], | |||
}), | |||
], | |||
false, | |||
], | |||
[ | |||
'upgrade event too old', | |||
[ | |||
mockAnalysis({ | |||
date: '2023-04-02T12:10:30+0200', | |||
events: [mockAnalysisEvent({ category: ProjectAnalysisEventCategory.SqUpgrade })], | |||
}), | |||
], | |||
false, | |||
], | |||
[ | |||
'upgrade event too far down in the list', | |||
[ | |||
mockAnalysis({ | |||
date: '2023-04-13T08:10:30+0200', | |||
}), | |||
mockAnalysis({ | |||
date: '2023-04-13T09:10:30+0200', | |||
}), | |||
mockAnalysis({ | |||
date: '2023-04-13T10:10:30+0200', | |||
}), | |||
mockAnalysis({ | |||
date: '2023-04-13T11:10:30+0200', | |||
}), | |||
mockAnalysis({ | |||
date: '2023-04-13T12:10:30+0200', | |||
events: [mockAnalysisEvent({ category: ProjectAnalysisEventCategory.SqUpgrade })], | |||
}), | |||
], | |||
false, | |||
], | |||
[ | |||
'upgrade event without QP update event', | |||
[ | |||
mockAnalysis({ | |||
date: '2023-04-13T12:10:30+0200', | |||
events: [mockAnalysisEvent({ category: ProjectAnalysisEventCategory.SqUpgrade })], | |||
}), | |||
], | |||
false, | |||
], | |||
[ | |||
'upgrade event with QP update event', | |||
[ | |||
mockAnalysis({ | |||
date: '2023-04-13T12:10:30+0200', | |||
events: [ | |||
mockAnalysisEvent({ category: ProjectAnalysisEventCategory.SqUpgrade }), | |||
mockAnalysisEvent({ category: ProjectAnalysisEventCategory.QualityProfile }), | |||
], | |||
}), | |||
], | |||
true, | |||
], | |||
])( | |||
'should correctly display message about SQ upgrade updating QPs', | |||
async (_, analyses, expected) => { | |||
jest.useFakeTimers({ | |||
advanceTimers: true, | |||
now: new Date('2023-04-25T12:00:00+0200'), | |||
}); | |||
jest.mocked(getProjectActivity).mockResolvedValueOnce({ | |||
analyses, | |||
} as { analyses: Analysis[]; paging: Paging }); | |||
renderBranchOverview(); | |||
await screen.findByText('overview.quality_gate.status'); | |||
expect( | |||
screen.queryByText(/overview.quality_profiles_update_after_sq_upgrade.message/) !== null, | |||
).toBe(expected); | |||
jest.useRealTimers(); | |||
}, | |||
); | |||
it('should correctly handle graph type storage', async () => { | |||
renderBranchOverview(); | |||
@@ -3872,6 +3872,9 @@ overview.badges.leak_warning=Project badges can expose your security rating and | |||
overview.badges.renew=Renew Token | |||
overview.badges.renew.description=If your project badge security token has leaked to an unsafe environment, you can renew it: | |||
overview.quality_profiles_update_after_sq_upgrade.message=Upgrade to SonarQube {sqVersion} has updated your Quality Profiles. Issues on your project may have been affected. {link} | |||
overview.quality_profiles_update_after_sq_upgrade.link=See more details | |||
#------------------------------------------------------------------------------ | |||
# |