diff options
author | Ismail Cherri <ismail.cherri@sonarsource.com> | 2024-10-09 15:20:46 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-10-16 20:02:59 +0000 |
commit | 3fedddcd21bfff1bff8e2eb25f34f110b803aa48 (patch) | |
tree | 6e7e9a253f80a8c955f462a49d6b6aa16dc85673 /server | |
parent | c8d599a0ee672ee5e7c5db2d88cb1b6482d65db7 (diff) | |
download | sonarqube-3fedddcd21bfff1bff8e2eb25f34f110b803aa48.tar.gz sonarqube-3fedddcd21bfff1bff8e2eb25f34f110b803aa48.zip |
SONAR-23194 Project overview adopts MQR mode
Diffstat (limited to 'server')
10 files changed, 146 insertions, 30 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx index 9e6b76baf78..92c9bd59815 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { BasicSeparator, Card, Spinner } from 'design-system'; +import { Spinner } from '@sonarsource/echoes-react'; +import { BasicSeparator, Card } from 'design-system'; import * as React from 'react'; import { MetricKey } from '~sonar-aligned/types/metrics'; import GraphsHeader from '../../../components/activity-graph/GraphsHeader'; @@ -32,6 +33,7 @@ import ActivityLink from '../../../components/common/ActivityLink'; import { parseDate } from '../../../helpers/dates'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { localizeMetric } from '../../../helpers/measures'; +import { useIsLegacyCCTMode } from '../../../queries/settings'; import { BranchLike } from '../../../types/branch-like'; import { Analysis as AnalysisType, @@ -70,7 +72,8 @@ export function ActivityPanel(props: ActivityPanelProps) { metrics, } = props; - const displayedMetrics = getDisplayedHistoryMetrics(graph, []); + const { data: isLegacy = false } = useIsLegacyCCTMode(); + const displayedMetrics = getDisplayedHistoryMetrics(graph, [], isLegacy); const series = generateSeries(measuresHistory, graph, metrics, displayedMetrics); const graphs = splitSeriesInGraphs(series, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH); let shownLeakPeriodDate; @@ -134,7 +137,7 @@ export function ActivityPanel(props: ActivityPanelProps) { <BasicSeparator className="sw-mb-4 sw-mt-16" /> - <Spinner loading={loading}> + <Spinner isLoading={loading}> {displayedAnalyses.length === 0 ? ( <p>{translate('no_results')}</p> ) : ( diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx index e7834509ce4..3a159842e5f 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx @@ -45,6 +45,7 @@ import { useApplicationQualityGateStatus, useProjectQualityGateStatus, } from '../../../queries/quality-gates'; +import { useIsLegacyCCTMode } from '../../../queries/settings'; import { ApplicationPeriod } from '../../../types/application'; import { Branch, BranchLike } from '../../../types/branch-like'; import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity'; @@ -68,6 +69,7 @@ const FROM_DATE = toISO8601WithOffsetString(new Date().setFullYear(new Date().ge export default function BranchOverview(props: Readonly<Props>) { const { component, branch, branchesEnabled } = props; + const { data: isLegacy = false } = useIsLegacyCCTMode(); const { graph: initialGraph } = getActivityGraph( BRANCH_OVERVIEW_ACTIVITY_GRAPH, props.component.key, @@ -281,7 +283,7 @@ export default function BranchOverview(props: Readonly<Props>) { }; const loadHistoryMeasures = React.useCallback(() => { - const graphMetrics = getHistoryMetrics(graph, []); + const graphMetrics = getHistoryMetrics(graph, [], isLegacy); const metrics = uniq([...HISTORY_METRICS_LIST, ...graphMetrics]); return getAllTimeMachineData({ @@ -375,11 +377,10 @@ export default function BranchOverview(props: Readonly<Props>) { }, [branch, loadHistory, loadStatus]); const projectIsEmpty = - loadingStatus === false && - (measures === undefined || - measures.find((measure) => - ([MetricKey.lines, MetricKey.new_lines] as string[]).includes(measure.metric.key), - ) === undefined); + !loadingStatus && + measures?.find((measure) => + ([MetricKey.lines, MetricKey.new_lines] as string[]).includes(measure.metric.key), + ) === undefined; return ( <BranchOverviewRenderer diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx index 6333d82a4ff..e6cd122544f 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx @@ -24,6 +24,7 @@ import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget'; import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter'; import { isPortfolioLike } from '~sonar-aligned/helpers/component'; import { ComponentQualifier } from '~sonar-aligned/types/component'; +import { MetricKey } from '~sonar-aligned/types/metrics'; import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures'; import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext'; import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage'; @@ -37,7 +38,6 @@ import { import { CodeScope } from '../../../helpers/urls'; import { useProjectAiCodeAssuredQuery } from '../../../queries/ai-code-assurance'; import { useDismissNoticeMutation } from '../../../queries/users'; -import { MetricKey } from '../../../sonar-aligned/types/metrics'; import { ApplicationPeriod } from '../../../types/application'; import { Branch } from '../../../types/branch-like'; import { isProject } from '../../../types/component'; @@ -83,7 +83,7 @@ export interface BranchOverviewRendererProps { qualityGate?: QualityGate; } -export default function BranchOverviewRenderer(props: BranchOverviewRendererProps) { +export default function BranchOverviewRenderer(props: Readonly<BranchOverviewRendererProps>) { const { analyses, appLeak, diff --git a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx index 0f4f9c97e80..3d381c9a414 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import styled from '@emotion/styled'; -import { LinkHighlight, LinkStandalone, Tooltip } from '@sonarsource/echoes-react'; -import { Badge, TextBold, TextSubdued, themeColor } from 'design-system'; +import { LinkHighlight, LinkStandalone, Text, Tooltip } from '@sonarsource/echoes-react'; +import { Badge, themeColor } from 'design-system'; import * as React from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { formatMeasure } from '~sonar-aligned/helpers/measures'; @@ -87,7 +87,8 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow > <div className="sw-flex sw-items-center"> <ColorBold className="sw-typo-semibold"> - {intl.formatMessage({ id: `software_quality.${softwareQuality}` })} + {!isLegacy && intl.formatMessage({ id: `software_quality.${softwareQuality}` })} + {alternativeMeasure && isLegacy && alternativeMeasure.metric.name} </ColorBold> {failed && ( <Badge className="sw-h-fit sw-ml-2" variant="deleted"> @@ -99,7 +100,7 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow <div className="sw-flex sw-mt-4"> <div className="sw-flex sw-gap-1 sw-items-center"> {count ? ( - <Tooltip content={countTooltipOverlay}> + <Tooltip content={countTooltipOverlay} isOpen={isLegacy ? false : undefined}> <LinkStandalone data-testid={`overview__software-impact-${softwareQuality}`} aria-label={intl.formatMessage( @@ -121,11 +122,11 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow </LinkStandalone> </Tooltip> ) : ( - <StyledDash className="sw-font-bold" name="-" /> + <StyledDash isHighlighted>-</StyledDash> )} - <TextSubdued className="sw-self-end sw-typo-default sw-pb-1"> + <Text isSubdued className="sw-self-end sw-typo-default sw-pb-1"> {intl.formatMessage({ id: 'overview.measures.software_impact.total_open_issues' })} - </TextSubdued> + </Text> </div> <div className="sw-flex-grow sw-flex sw-justify-end"> @@ -142,7 +143,7 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow ); } -const StyledDash = styled(TextBold)` +const StyledDash = styled(Text)` font-size: 36px; `; const ColorBold = styled.h2` diff --git a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx index b5eeb3d48af..c58ff188d11 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx @@ -21,8 +21,10 @@ import { RatingEnum } from 'design-system/lib'; import * as React from 'react'; import { useCallback } from 'react'; import { useIntl } from 'react-intl'; +import { MetricKey } from '~sonar-aligned/types/metrics'; import RatingComponent from '../../../app/components/metrics/RatingComponent'; -import { MetricKey } from '../../../sonar-aligned/types/metrics'; +import RatingTooltipContent from '../../../components/measure/RatingTooltipContent'; +import { useIsLegacyCCTMode } from '../../../queries/settings'; import { Branch } from '../../../types/branch-like'; import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy'; @@ -35,15 +37,20 @@ export interface SoftwareImpactMeasureRatingProps { export function SoftwareImpactMeasureRating(props: Readonly<SoftwareImpactMeasureRatingProps>) { const { ratingMetricKey, componentKey, softwareQuality, branch } = props; + const { data: isLegacy = false } = useIsLegacyCCTMode(); const intl = useIntl(); const getSoftwareImpactRatingTooltip = useCallback( - (rating: RatingEnum) => { + (rating: RatingEnum, value: string | undefined) => { if (rating === undefined) { return null; } + if (isLegacy && value !== undefined) { + return <RatingTooltipContent metricKey={ratingMetricKey} value={value} />; + } + function ratingToWorseSeverity(rating: string): SoftwareImpactSeverity { return ( { diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ActivityPanel-it.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ActivityPanel-it.tsx index 8b49e156574..b250935267b 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ActivityPanel-it.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ActivityPanel-it.tsx @@ -18,7 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { screen } from '@testing-library/react'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import * as React from 'react'; +import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock'; import { mockComponent } from '../../../../helpers/mocks/component'; import { mockAnalysis, @@ -38,10 +40,16 @@ import { DefinitionChangeType, ProjectAnalysisEventCategory, } from '../../../../types/project-activity'; +import { SettingsKey } from '../../../../types/settings'; import ActivityPanel, { ActivityPanelProps } from '../ActivityPanel'; -it('should render correctly', async () => { - const user = userEvent.setup(); +const settingsHandler = new SettingsServiceMock(); + +afterEach(() => { + settingsHandler.reset(); +}); + +async function expectGraphs(user: UserEvent) { renderActivityPanel(); expect(await screen.findAllByText('metric.level.ERROR')).toHaveLength(2); @@ -52,7 +60,7 @@ it('should render correctly', async () => { expect(screen.getByText('event.sqUpgrade10.2')).toBeInTheDocument(); // Checking measures variations - expect(screen.getAllByText(/% project_activity\.graphs\.coverage$/)).toHaveLength(3); + expect(await screen.findAllByText(/% project_activity\.graphs\.coverage$/)).toHaveLength(3); expect(screen.getAllByText(/% project_activity\.graphs\.duplications$/)).toHaveLength(3); // Analysis 1 (latest) expect(screen.getByText(/^-5 project_activity\.graphs\.issues$/)).toBeInTheDocument(); @@ -76,6 +84,10 @@ it('should render correctly', async () => { ), ).toBeInTheDocument(); + return user; +} + +async function expectQualityGate(user: UserEvent) { await user.click( screen.getByRole('link', { name: 'quality_profiles.page_title_changelog_x.QP-test: 1 new rule, 2 modified rules, and 3 removed rules', @@ -83,11 +95,61 @@ it('should render correctly', async () => { ); expect(await screen.findByText('QP-test java')).toBeInTheDocument(); +} + +it('should render correctly', async () => { + const user = userEvent.setup(); + await expectGraphs(user); + + expect(screen.queryByText('metric.code_smells.name')).not.toBeInTheDocument(); + expect(screen.queryByText('metric.vulnerabilities.name')).not.toBeInTheDocument(); + expect(screen.queryByText('metric.bugs.name')).not.toBeInTheDocument(); + + await expectQualityGate(user); +}); + +it('should render correctly for legacy mode', async () => { + settingsHandler.set(SettingsKey.MQRMode, 'false'); + const user = userEvent.setup(); + await expectGraphs(user); + + expect(screen.getByText('metric.code_smells.name')).toBeInTheDocument(); + expect(screen.getByText('metric.vulnerabilities.name')).toBeInTheDocument(); + expect(screen.getByText('metric.bugs.name')).toBeInTheDocument(); + + await expectQualityGate(user); }); function renderActivityPanel() { const mockedMeasureHistory = [ mockMeasureHistory({ + metric: MetricKey.vulnerabilities, + history: [ + mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '200' }), + mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '200' }), + mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '100' }), + mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '95' }), + ], + }), + mockMeasureHistory({ + metric: MetricKey.code_smells, + history: [ + mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '200' }), + mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '200' }), + mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '100' }), + mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '95' }), + ], + }), + mockMeasureHistory({ + metric: MetricKey.bugs, + history: [ + mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '200' }), + mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '200' }), + mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '100' }), + mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '95' }), + ], + }), + mockMeasureHistory({ metric: MetricKey.violations, history: [ mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '200' }), diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx index 78d22c7c9ec..35d1150b9cd 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx @@ -37,7 +37,7 @@ interface Props { selectedMetrics?: string[]; } -export default function GraphsHeader(props: Props) { +export default function GraphsHeader(props: Readonly<Props>) { const { className, graph, diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts b/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts index 4cfdf423674..a289f2bae52 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts @@ -105,6 +105,13 @@ describe('getDisplayedHistoryMetrics', () => { customMetrics, ); }); + it('should return Legacy graphs', () => { + expect(utils.getDisplayedHistoryMetrics(GraphType.issues, [], true)).toEqual([ + MetricKey.bugs, + MetricKey.code_smells, + MetricKey.vulnerabilities, + ]); + }); }); describe('getHistoryMetrics', () => { @@ -123,6 +130,16 @@ describe('getHistoryMetrics', () => { ]); expect(utils.getHistoryMetrics(GraphType.custom, customMetrics)).toEqual(customMetrics); }); + it('should return legacy metrics', () => { + expect(utils.getHistoryMetrics(utils.DEFAULT_GRAPH, [], true)).toEqual([ + MetricKey.bugs, + MetricKey.code_smells, + MetricKey.vulnerabilities, + MetricKey.reliability_rating, + MetricKey.security_rating, + MetricKey.sqale_rating, + ]); + }); }); describe('hasHistoryData', () => { diff --git a/server/sonar-web/src/main/js/components/activity-graph/utils.ts b/server/sonar-web/src/main/js/components/activity-graph/utils.ts index 54dbc17e42c..b1768446382 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/utils.ts +++ b/server/sonar-web/src/main/js/components/activity-graph/utils.ts @@ -37,6 +37,11 @@ const GRAPHS_METRICS_DISPLAYED: Dict<string[]> = { [GraphType.duplications]: [MetricKey.ncloc, MetricKey.duplicated_lines], }; +const LEGACY_GRAPHS_METRICS_DISPLAYED: Dict<string[]> = { + ...GRAPHS_METRICS_DISPLAYED, + [GraphType.issues]: [MetricKey.bugs, MetricKey.code_smells, MetricKey.vulnerabilities], +}; + const GRAPHS_METRICS: Dict<string[]> = { [GraphType.issues]: GRAPHS_METRICS_DISPLAYED[GraphType.issues].concat([ MetricKey.reliability_rating, @@ -50,6 +55,15 @@ const GRAPHS_METRICS: Dict<string[]> = { ], }; +const LEGACY_GRAPHS_METRICS: Dict<string[]> = { + ...GRAPHS_METRICS, + [GraphType.issues]: LEGACY_GRAPHS_METRICS_DISPLAYED[GraphType.issues].concat([ + MetricKey.reliability_rating, + MetricKey.security_rating, + MetricKey.sqale_rating, + ]), +}; + export const LINE_CHART_DASHES = [0, 3, 7]; export function isCustomGraph(graph: GraphType) { @@ -74,12 +88,23 @@ export function getSeriesMetricType(series: Serie[]) { return series.length > 0 ? series[0].type : MetricType.Integer; } -export function getDisplayedHistoryMetrics(graph: GraphType, customMetrics: string[]) { - return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS_DISPLAYED[graph]; +export function getDisplayedHistoryMetrics( + graph: GraphType, + customMetrics: string[], + isLegacy = false, +) { + if (isCustomGraph(graph)) { + return customMetrics; + } + + return isLegacy ? LEGACY_GRAPHS_METRICS_DISPLAYED[graph] : GRAPHS_METRICS_DISPLAYED[graph]; } -export function getHistoryMetrics(graph: GraphType, customMetrics: string[]) { - return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS[graph]; +export function getHistoryMetrics(graph: GraphType, customMetrics: string[], isLegacy = false) { + if (isCustomGraph(graph)) { + return customMetrics; + } + return isLegacy ? LEGACY_GRAPHS_METRICS[graph] : GRAPHS_METRICS[graph]; } export function hasHistoryDataValue(series: Serie[]) { diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/telemetry/TelemetryMQRModePropertyProvider.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/telemetry/TelemetryMQRModePropertyProvider.java index ed1e6ab34fe..b827a02f278 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/telemetry/TelemetryMQRModePropertyProvider.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/telemetry/TelemetryMQRModePropertyProvider.java @@ -62,6 +62,6 @@ public class TelemetryMQRModePropertyProvider implements TelemetryDataProvider<B @Override public Optional<Boolean> getValue() { PropertyDto property = dbClient.propertiesDao().selectGlobalProperty(MULTI_QUALITY_MODE_ENABLED); - return Optional.of(property != null && Boolean.parseBoolean(property.getValue())); + return property == null ? Optional.of(true) : Optional.of(Boolean.valueOf(property.getValue())); } } |