aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/app/components/ChangeInCalculationPill.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/CalculationChangeMessage-test.tsx25
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/calculation-notification/CalculationChangeMessage.tsx15
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx197
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx39
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx65
-rw-r--r--server/sonar-web/src/main/js/components/facets/FacetHelp.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/shared/AnalysisMissingInfoMessage.tsx61
-rw-r--r--server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/shared/ComponentMissingMqrMetricsMessage.tsx90
-rw-r--r--server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx2
16 files changed, 323 insertions, 236 deletions
diff --git a/server/sonar-web/src/main/js/app/components/ChangeInCalculationPill.tsx b/server/sonar-web/src/main/js/app/components/ChangeInCalculationPill.tsx
index 69251952790..cb708ba4639 100644
--- a/server/sonar-web/src/main/js/app/components/ChangeInCalculationPill.tsx
+++ b/server/sonar-web/src/main/js/app/components/ChangeInCalculationPill.tsx
@@ -19,7 +19,7 @@
*/
import { Popover } from '@sonarsource/echoes-react';
-import * as React from 'react';
+import { noop } from 'lodash';
import { Pill, PillVariant } from '~design-system';
import DocumentationLink from '../../components/common/DocumentationLink';
import { DocLink } from '../../helpers/doc-links';
@@ -32,7 +32,6 @@ interface Props {
}
export default function ChangeInCalculation({ qualifier }: Readonly<Props>) {
- const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const { data: isStandardMode, isLoading } = useStandardExperienceModeQuery();
if (isStandardMode || isLoading) {
@@ -41,20 +40,15 @@ export default function ChangeInCalculation({ qualifier }: Readonly<Props>) {
return (
<Popover
- isOpen={isPopoverOpen}
title={translate('projects.awaiting_scan.title')}
description={translate(`projects.awaiting_scan.description.${qualifier}`)}
footer={
- <DocumentationLink to={DocLink.CleanCodeIntroduction}>
- {translate('learn_more')}
+ <DocumentationLink shouldOpenInNewTab standalone to={DocLink.MetricDefinitions}>
+ {translate('projects.awaiting_scan.learn_more')}
</DocumentationLink>
}
>
- <Pill
- variant={PillVariant.Info}
- className="sw-ml-2"
- onClick={() => setIsPopoverOpen(!isPopoverOpen)}
- >
+ <Pill variant={PillVariant.Info} className="sw-ml-2" onClick={noop}>
{translate('projects.awaiting_scan')}
</Pill>
</Popover>
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/CalculationChangeMessage-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/CalculationChangeMessage-test.tsx
index 47383932bae..aac469f7bee 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/CalculationChangeMessage-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/CalculationChangeMessage-test.tsx
@@ -20,7 +20,6 @@
import { Outlet, Route } from 'react-router-dom';
import { byRole, byText } from '~sonar-aligned/helpers/testSelector';
-import { ComponentQualifier } from '~sonar-aligned/types/component';
import { ModeServiceMock } from '../../../api/mocks/ModeServiceMock';
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { Mode } from '../../../types/mode';
@@ -28,9 +27,11 @@ import CalculationChangeMessage from '../calculation-notification/CalculationCha
const ui = {
alert: byRole('alert'),
- learnMoreLink: byRole('link', { name: 'learn_more' }),
+ learnMoreLink: byRole('link', {
+ name: 'notification.calculation_change.message_link open_in_new_tab',
+ }),
- alertText: (qualifier: string) => byText(`notification.calculation_change.message.${qualifier}`),
+ alertText: byText('notification.calculation_change.message'),
};
const modeHandler = new ModeServiceMock();
@@ -40,30 +41,30 @@ beforeEach(() => {
});
it.each([
- ['Project', '/projects', ComponentQualifier.Project],
- ['Portfolios', '/portfolios', ComponentQualifier.Portfolio],
-])('should render on %s page', (_, path, qualifier) => {
+ ['Project', '/projects'],
+ ['Portfolios', '/portfolios'],
+])('should render on %s page', (_, path) => {
render(path);
expect(ui.alert.get()).toBeInTheDocument();
- expect(ui.alertText(qualifier).get()).toBeInTheDocument();
+ expect(ui.alertText.get()).toBeInTheDocument();
expect(ui.learnMoreLink.get()).toBeInTheDocument();
});
it.each([
- ['Project', '/projects', ComponentQualifier.Project],
- ['Portfolios', '/portfolios', ComponentQualifier.Portfolio],
-])('should not render on %s page if isStandardMode', (_, path, qualifier) => {
+ ['Project', '/projects'],
+ ['Portfolios', '/portfolios'],
+])('should not render on %s page if isStandardMode', (_, path) => {
modeHandler.setMode(Mode.Standard);
render(path);
expect(ui.alert.get()).toBeInTheDocument();
- expect(ui.alertText(qualifier).get()).toBeInTheDocument();
+ expect(ui.alertText.get()).toBeInTheDocument();
expect(ui.learnMoreLink.get()).toBeInTheDocument();
});
it('should not render on other page', () => {
render('/other');
expect(ui.alert.query()).not.toBeInTheDocument();
- expect(ui.alertText(ComponentQualifier.Project).query()).not.toBeInTheDocument();
+ expect(ui.alertText.query()).not.toBeInTheDocument();
expect(ui.learnMoreLink.query()).not.toBeInTheDocument();
});
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
index 2b0e1796474..ba421059107 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
@@ -53,7 +53,9 @@ jest.mock('../../../api/components', () => ({
}));
jest.mock('../../../queries/mode', () => ({
- useStandardExperienceModeQuery: jest.fn(),
+ useStandardExperienceModeQuery: jest.fn(() => ({
+ data: { mode: 'STANDARD_EXPERIENCE', modified: false },
+ })),
}));
jest.mock('../../../api/navigation', () => ({
diff --git a/server/sonar-web/src/main/js/app/components/calculation-notification/CalculationChangeMessage.tsx b/server/sonar-web/src/main/js/app/components/calculation-notification/CalculationChangeMessage.tsx
index 3252f4890e0..8256becdc22 100644
--- a/server/sonar-web/src/main/js/app/components/calculation-notification/CalculationChangeMessage.tsx
+++ b/server/sonar-web/src/main/js/app/components/calculation-notification/CalculationChangeMessage.tsx
@@ -18,13 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { LinkHighlight } from '@sonarsource/echoes-react';
import { FormattedMessage } from 'react-intl';
import { useLocation } from '~sonar-aligned/components/hoc/withRouter';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import DocumentationLink from '../../../components/common/DocumentationLink';
import { DismissableAlert } from '../../../components/ui/DismissableAlert';
import { DocLink } from '../../../helpers/doc-links';
-import { translate } from '../../../helpers/l10n';
import { useStandardExperienceModeQuery } from '../../../queries/mode';
import { Dict } from '../../../types/types';
@@ -46,11 +46,16 @@ export default function CalculationChangeMessage() {
return (
<DismissableAlert variant="info" alertKey={ALERT_KEY + SHOW_MESSAGE_PATHS[location.pathname]}>
<FormattedMessage
- id={`notification.calculation_change.message.${SHOW_MESSAGE_PATHS[location.pathname]}`}
+ id="notification.calculation_change.message"
values={{
- link: (
- <DocumentationLink className="sw-ml-1" to={DocLink.MetricDefinitions}>
- {translate('learn_more')}
+ link: (text) => (
+ <DocumentationLink
+ shouldOpenInNewTab
+ className="sw-ml-1"
+ highlight={LinkHighlight.Default}
+ to={DocLink.MetricDefinitions}
+ >
+ {text}
</DocumentationLink>
),
}}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
index 35e1e25dfa7..67c27da7acd 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
@@ -22,6 +22,7 @@ import * as React from 'react';
import { TopBar } from '~design-system';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import NCDAutoUpdateMessage from '../../../../components/new-code-definition/NCDAutoUpdateMessage';
+import { ComponentMissingMqrMetricsMessage } from '../../../../components/shared/ComponentMissingMqrMetricsMessage';
import { getBranchLikeDisplayName } from '../../../../helpers/branch-like';
import { translate } from '../../../../helpers/l10n';
import { isDefined } from '../../../../helpers/types';
@@ -77,6 +78,7 @@ function ComponentNav(props: Readonly<ComponentNavProps>) {
<Menu component={component} isInProgress={isInProgress} isPending={isPending} />
</TopBar>
<NCDAutoUpdateMessage branchName={branchName} component={component} />
+ <ComponentMissingMqrMetricsMessage component={component} />
{projectBindingErrors !== undefined && (
<ComponentNavProjectBindingErrorNotif component={component} />
)}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
index 96deda8942a..067866c1ee7 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
@@ -18,22 +18,39 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock';
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
+import { MeasuresServiceMock } from '../../../../../api/mocks/MeasuresServiceMock';
+import { ModeServiceMock } from '../../../../../api/mocks/ModeServiceMock';
import { mockProjectAlmBindingConfigurationErrors } from '../../../../../helpers/mocks/alm-settings';
import { mockComponent } from '../../../../../helpers/mocks/component';
+import { get } from '../../../../../helpers/storage';
+import { mockMeasure } from '../../../../../helpers/testMocks';
import { renderApp } from '../../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../../sonar-aligned/helpers/testSelector';
+import { MetricKey } from '../../../../../sonar-aligned/types/metrics';
+import { Mode } from '../../../../../types/mode';
import ComponentNav, { ComponentNavProps } from '../ComponentNav';
+jest.mock('../../../../../helpers/storage', () => ({
+ get: jest.fn(),
+ remove: jest.fn(),
+ save: jest.fn(),
+}));
+
const branchesHandler = new BranchesServiceMock();
const almHandler = new AlmSettingsServiceMock();
+const modeHandler = new ModeServiceMock();
+const measuresHandler = new MeasuresServiceMock();
afterEach(() => {
branchesHandler.reset();
almHandler.reset();
+ modeHandler.reset();
+ measuresHandler.reset();
});
it('renders correctly when the project binding is incorrect', () => {
@@ -52,16 +69,178 @@ it('correctly returns focus to the Project Information link when the drawer is c
expect(await screen.findByText('/project/information?id=my-project')).toBeInTheDocument();
});
+describe('MQR mode calculation change message', () => {
+ it('does not render the message in standard mode', async () => {
+ modeHandler.setMode(Mode.Standard);
+ renderComponentNav();
+
+ await waitFor(() => {
+ expect(screen.queryByText(/overview.missing_project_data/)).not.toBeInTheDocument();
+ });
+ });
+
+ it.each([
+ ['project', ComponentQualifier.Project],
+ ['application', ComponentQualifier.Application],
+ ['portfolio', ComponentQualifier.Portfolio],
+ ])('does not render message when %s is not computed', async (_, qualifier) => {
+ const component = mockComponent({
+ qualifier,
+ breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier }],
+ });
+ measuresHandler.registerComponentMeasures({
+ [component.key]: {},
+ });
+ renderComponentNav({ component });
+
+ await waitFor(() => {
+ expect(
+ byRole('alert')
+ .byText(new RegExp(`overview.missing_project_data${qualifier}`))
+ .query(),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ it.each([
+ ['project', ComponentQualifier.Project],
+ ['application', ComponentQualifier.Application],
+ ['portfolio', ComponentQualifier.Portfolio],
+ ])('does not render message when %s mqr metrics computed', async (_, qualifier) => {
+ const component = mockComponent({
+ qualifier,
+ breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier }],
+ });
+ measuresHandler.registerComponentMeasures({
+ [component.key]: {
+ [MetricKey.security_rating]: mockMeasure({
+ metric: MetricKey.security_rating,
+ value: '1.0',
+ }),
+ [MetricKey.software_quality_security_rating]: mockMeasure({
+ metric: MetricKey.software_quality_security_rating,
+ value: '1.0',
+ }),
+ },
+ });
+ renderComponentNav({ component });
+
+ await waitFor(() => {
+ expect(
+ byRole('alert')
+ .byText(new RegExp(`overview.missing_project_data${qualifier}`))
+ .query(),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ it.each([
+ ['project', ComponentQualifier.Project],
+ ['application', ComponentQualifier.Application],
+ ['portfolio', ComponentQualifier.Portfolio],
+ ])(
+ 'does not render message when %s mqr metrics are not computed but it was already dismissed',
+ async (_, qualifier) => {
+ const component = mockComponent({
+ qualifier,
+ breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier }],
+ });
+ jest.mocked(get).mockImplementation((key) => {
+ const keys: Record<string, string> = {
+ [`sonarqube.dismissed_calculation_change_alert.component_${component.key}`]: 'true',
+ };
+ return keys[key];
+ });
+ measuresHandler.registerComponentMeasures({
+ [component.key]: {
+ [MetricKey.security_rating]: mockMeasure({
+ metric: MetricKey.security_rating,
+ value: '1.0',
+ }),
+ },
+ });
+ renderComponentNav({ component });
+
+ await waitFor(() => {
+ expect(
+ byRole('alert')
+ .byText(new RegExp(`overview.missing_project_data${qualifier}`))
+ .query(),
+ ).not.toBeInTheDocument();
+ });
+ jest.mocked(get).mockRestore();
+ },
+ );
+
+ it.each([
+ ['project', ComponentQualifier.Project],
+ ['application', ComponentQualifier.Application],
+ ['portfolio', ComponentQualifier.Portfolio],
+ ])('renders message when %s mqr metrics are not computed', async (_, qualifier) => {
+ const component = mockComponent({
+ qualifier,
+ breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier }],
+ });
+ measuresHandler.registerComponentMeasures({
+ [component.key]: {
+ [MetricKey.security_rating]: mockMeasure({
+ metric: MetricKey.security_rating,
+ value: '1.0',
+ }),
+ },
+ });
+ renderComponentNav({ component });
+
+ expect(
+ await byRole('alert')
+ .byText(new RegExp(`overview.missing_project_data${qualifier}`))
+ .find(),
+ ).toBeInTheDocument();
+
+ expect(
+ byRole('link', { name: /overview.missing_project_data_link/ }).get(),
+ ).toBeInTheDocument();
+ });
+
+ it('can dismiss message', async () => {
+ const user = userEvent.setup();
+
+ measuresHandler.registerComponentMeasures({
+ 'my-project': {
+ [MetricKey.security_rating]: mockMeasure({
+ metric: MetricKey.security_rating,
+ value: '1.0',
+ }),
+ },
+ });
+ renderComponentNav();
+ expect(
+ await byRole('alert')
+ .byText(/overview.missing_project_dataTRK/)
+ .find(),
+ ).toBeInTheDocument();
+
+ await user.click(byRole('button', { name: 'dismiss' }).get());
+
+ expect(
+ byRole('alert')
+ .byText(/overview.missing_project_dataTRK/)
+ .query(),
+ ).not.toBeInTheDocument();
+ });
+});
+
function renderComponentNav(props: Partial<ComponentNavProps> = {}) {
+ const component =
+ props.component ??
+ mockComponent({
+ breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }],
+ });
+
+ measuresHandler.setComponents({ component, ancestors: [], children: [] });
+
return renderApp(
'/',
- <ComponentNav
- component={mockComponent({
- breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }],
- })}
- isInProgress={false}
- isPending={false}
- {...props}
- />,
+ <ComponentNav isInProgress={false} isPending={false} {...props} component={component} />,
);
}
diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
index 65598609512..f77bf5e01d9 100644
--- a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
@@ -28,7 +28,6 @@ import { isPortfolioLike } from '~sonar-aligned/helpers/component';
import { Breadcrumb } from '~sonar-aligned/types/component';
import { Location } from '~sonar-aligned/types/router';
import ListFooter from '../../../components/controls/ListFooter';
-import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
import {
CCT_SOFTWARE_QUALITY_METRICS,
LEAK_OLD_TAXONOMY_RATINGS,
@@ -152,14 +151,6 @@ export default function CodeAppRenderer(props: Readonly<Props>) {
)}
<Spinner isLoading={loading || isLoadingStandardMode}>
- {!allComponentsHaveSoftwareQualityMeasures && (
- <AnalysisMissingInfoMessage
- qualifier={component.qualifier}
- hide={isPortfolio}
- className="sw-mb-4"
- />
- )}
-
<div className="sw-flex sw-justify-between">
<div>
{hasComponents && (
diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
index a17a7a81b76..450528bed0e 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
@@ -184,7 +184,6 @@ describe('rendering', () => {
].forEach((measure) => {
expect(ui.measureLink(measure).get()).toBeInTheDocument();
});
- expect(ui.analysisMissingMessage.get()).toBeInTheDocument();
});
it('should show new counts but not ratings if no rating measures', async () => {
@@ -216,7 +215,6 @@ describe('rendering', () => {
].forEach((measure) => {
expect(ui.measureLink(measure).get()).toBeInTheDocument();
});
- expect(ui.analysisMissingMessage.get()).toBeInTheDocument();
});
it('should show old measures and no flag message if no rating measures and legacy mode', async () => {
@@ -361,18 +359,6 @@ describe('rendering', () => {
expect(screen.queryByText('overview.missing_project_dataTRK')).not.toBeInTheDocument();
});
- it('should render analysis missing if on a pull request and leak measure are missing', async () => {
- const { ui } = getPageObject();
- measuresHandler.deleteComponentMeasure(
- 'foo',
- MetricKey.new_software_quality_maintainability_rating,
- );
- renderMeasuresApp('component_measures?id=foo&pullRequest=01');
- await ui.appLoaded();
-
- expect(ui.analysisMissingMessage.get()).toBeInTheDocument();
- });
-
it('should render a warning message if the user does not have access to all components', async () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=code_smells', {
@@ -713,7 +699,6 @@ function getPageObject() {
seeDataAsListLink: byRole('link', { name: 'component_measures.overview.see_data_as_list' }),
bubbleChart: byTestId('bubble-chart'),
newCodePeriodTxt: byText('component_measures.leak_legend.new_code'),
- analysisMissingMessage: byText('overview.missing_project_dataTRK'),
// Navigation
overviewDomainLink: byRole('link', {
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
index f9374ff0488..ec954ba676c 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
@@ -42,14 +42,7 @@ import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { enhanceMeasure } from '../../../components/measure/utils';
import '../../../components/search-navigator.css';
-import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
import { translate } from '../../../helpers/l10n';
-import {
- areCCTMeasuresComputed,
- areLeakCCTMeasuresComputed,
- areLeakSoftwareQualityRatingsComputed,
- areSoftwareQualityRatingsComputed,
-} from '../../../helpers/measures';
import { useCurrentBranchQuery } from '../../../queries/branch';
import { useMeasuresComponentQuery } from '../../../queries/measures';
@@ -106,10 +99,6 @@ export default function ComponentMeasuresApp() {
componentWithMeasures?.qualifier === ComponentQualifier.Project ? period : undefined;
const displayOverview = hasBubbleChart(bubblesByDomain, query.metric);
- const showMissingAnalysisMessage = isPullRequest(branchLike)
- ? !areLeakCCTMeasuresComputed(measures) || !areLeakSoftwareQualityRatingsComputed(measures)
- : !areCCTMeasuresComputed(measures) || !areSoftwareQualityRatingsComputed(measures);
-
if (!component) {
return null;
}
@@ -232,12 +221,6 @@ export default function ComponentMeasuresApp() {
/>
</FlagMessage>
)}
- {showMissingAnalysisMessage && (
- <AnalysisMissingInfoMessage
- className="sw-mb-4"
- qualifier={component?.qualifier as ComponentQualifier}
- />
- )}
{renderContent()}
</div>
</div>
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 af7bdc781d5..f967c9fe884 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
@@ -23,17 +23,11 @@ import { useState } from 'react';
import { CardSeparator, CenteredLayout, PageContentFontWrapper } from '~design-system';
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 { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
-import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
import { parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
-import {
- areCCTMeasuresComputed,
- areSoftwareQualityRatingsComputed,
- isDiffMetric,
-} from '../../../helpers/measures';
+import { isDiffMetric } from '../../../helpers/measures';
import { CodeScope } from '../../../helpers/urls';
import { useDismissNoticeMutation } from '../../../queries/users';
import { ApplicationPeriod } from '../../../types/application';
@@ -119,10 +113,6 @@ export default function BranchOverviewRenderer(props: Readonly<BranchOverviewRen
const isNewCodeTab = tab === CodeScope.New;
const hasNewCodeMeasures = measures.some((m) => isDiffMetric(m.metric.key));
- // Check if any potentially missing uncomputed measure is not present
- const isMissingMeasures =
- !areCCTMeasuresComputed(measures) || !areSoftwareQualityRatingsComputed(measures);
-
const selectTab = (tab: CodeScope) => {
router.replace({ query: { ...query, codeScope: tab } });
};
@@ -137,14 +127,6 @@ export default function BranchOverviewRenderer(props: Readonly<BranchOverviewRen
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [loadingStatus, hasNewCodeMeasures]);
- const analysisMissingInfo = isMissingMeasures && (
- <AnalysisMissingInfoMessage
- qualifier={component.qualifier}
- hide={isPortfolioLike(component.qualifier)}
- className="sw-mb-8"
- />
- );
-
const dismissPromotedSection = () => {
dismissNotice(NoticeType.ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE);
@@ -268,17 +250,14 @@ export default function BranchOverviewRenderer(props: Readonly<BranchOverviewRen
)}
{!isNewCodeTab && (
- <>
- {analysisMissingInfo}
- <OverallCodeMeasuresPanel
- branch={branch}
- qgStatuses={qgStatuses}
- component={component}
- measures={measures}
- loading={loadingStatus}
- qualityGate={qualityGate}
- />
- </>
+ <OverallCodeMeasuresPanel
+ branch={branch}
+ qgStatuses={qgStatuses}
+ component={component}
+ measures={measures}
+ loading={loadingStatus}
+ qualityGate={qualityGate}
+ />
)}
</TabsPanel>
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
index 2ea4ed1d035..f39d62b0d08 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
@@ -422,30 +422,7 @@ describe('project overview', () => {
).toBeInTheDocument();
});
- it.each([
- [MetricKey.software_quality_security_issues],
- [MetricKey.software_quality_reliability_issues],
- [MetricKey.software_quality_maintainability_issues],
- ])(
- 'should display info about missing analysis if a project is not computed for %s',
- async (missingMetricKey) => {
- measuresHandler.deleteComponentMeasure('foo', missingMetricKey);
- const { user, ui } = getPageObjects();
- renderBranchOverview();
-
- await user.click(await ui.overallCodeButton.find());
-
- expect(
- await ui.softwareImpactMeasureCard(SoftwareQuality.Security).find(),
- ).toBeInTheDocument();
-
- await user.click(await ui.overallCodeButton.find());
-
- expect(await screen.findByText('overview.missing_project_dataTRK')).toBeInTheDocument();
- },
- );
-
- it('should display info about missing analysis if a project did not compute ratings', async () => {
+ it('should display standard ratings if a project did not compute mqr ratings', async () => {
measuresHandler.deleteComponentMeasure('foo', MetricKey.software_quality_security_rating);
measuresHandler.deleteComponentMeasure(
'foo',
@@ -461,7 +438,6 @@ describe('project overview', () => {
await user.click(await ui.overallCodeButton.find());
- expect(await screen.findByText('overview.missing_project_dataTRK')).toBeInTheDocument();
ui.expectSoftwareImpactMeasureCard(SoftwareQuality.Security);
expect(
ui.softwareImpactMeasureCardRating(SoftwareQuality.Security, 'B').get(),
@@ -511,28 +487,6 @@ describe('project overview', () => {
);
});
- it('should not show analysis is missing message in legacy mode', async () => {
- measuresHandler.deleteComponentMeasure('foo', MetricKey.software_quality_security_rating);
- measuresHandler.deleteComponentMeasure(
- 'foo',
- MetricKey.software_quality_maintainability_rating,
- );
- measuresHandler.deleteComponentMeasure('foo', MetricKey.software_quality_reliability_rating);
- modeHandler.setMode(Mode.Standard);
- const { user, ui } = getPageObjects();
- renderBranchOverview();
-
- await user.click(await ui.overallCodeButton.find());
-
- expect(await ui.softwareImpactMeasureCard(SoftwareQuality.Security).find()).toBeInTheDocument();
-
- await user.click(await ui.overallCodeButton.find());
-
- expect(await ui.softwareImpactMeasureCard(SoftwareQuality.Security).find()).toBeInTheDocument();
-
- expect(screen.queryByText('overview.missing_project_dataTRK')).not.toBeInTheDocument();
- });
-
it('should dismiss CaYC promoted section', async () => {
qualityGatesHandler.setQualityGateProjectStatus(
mockQualityGateProjectStatus({
@@ -713,23 +667,6 @@ describe('application overview', () => {
expect(await screen.findByText('portfolio.app.empty')).toBeInTheDocument();
});
-
- it.each([
- [MetricKey.software_quality_security_issues],
- [MetricKey.software_quality_reliability_issues],
- [MetricKey.software_quality_maintainability_issues],
- ])(
- 'should ask to reanalyze all projects if a project is not computed for %s',
- async (missingMetricKey) => {
- const { ui, user } = getPageObjects();
-
- measuresHandler.deleteComponentMeasure('foo', missingMetricKey as MetricKey);
- renderBranchOverview({ component });
- await user.click(await ui.overallCodeButton.find());
-
- expect(await screen.findByText('overview.missing_project_dataAPP')).toBeInTheDocument();
- },
- );
});
it.each([
diff --git a/server/sonar-web/src/main/js/components/facets/FacetHelp.tsx b/server/sonar-web/src/main/js/components/facets/FacetHelp.tsx
index 836dc84e12a..7c7609700ef 100644
--- a/server/sonar-web/src/main/js/components/facets/FacetHelp.tsx
+++ b/server/sonar-web/src/main/js/components/facets/FacetHelp.tsx
@@ -60,7 +60,7 @@ export function FacetHelp({ property, title, description, noDescription, link, l
: description
}
footer={
- <DocumentationLink standalone to={link}>
+ <DocumentationLink shouldOpenInNewTab standalone to={link}>
{property ? intl.formatMessage({ id: `issues.facet.${property}.help.link` }) : linkText}
</DocumentationLink>
}
diff --git a/server/sonar-web/src/main/js/components/shared/AnalysisMissingInfoMessage.tsx b/server/sonar-web/src/main/js/components/shared/AnalysisMissingInfoMessage.tsx
deleted file mode 100644
index e59f81169be..00000000000
--- a/server/sonar-web/src/main/js/components/shared/AnalysisMissingInfoMessage.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { FormattedMessage, useIntl } from 'react-intl';
-import { FlagMessage } from '~design-system';
-import { DocLink } from '../../helpers/doc-links';
-import { useStandardExperienceModeQuery } from '../../queries/mode';
-import DocumentationLink from '../common/DocumentationLink';
-
-interface AnalysisMissingInfoMessageProps {
- className?: string;
- hide?: boolean;
- qualifier: string;
-}
-
-export default function AnalysisMissingInfoMessage({
- hide,
- qualifier,
- className,
-}: Readonly<AnalysisMissingInfoMessageProps>) {
- const { data: isStandardMode, isLoading } = useStandardExperienceModeQuery();
- const intl = useIntl();
-
- if (hide || isLoading || isStandardMode) {
- return null;
- }
-
- return (
- <FlagMessage variant="info" className={className}>
- <FormattedMessage
- id="overview.missing_project_data"
- tagName="div"
- values={{
- qualifier,
- learn_more: (
- <DocumentationLink className="sw-whitespace-nowrap" to={DocLink.CodeAnalysis}>
- {intl.formatMessage({ id: 'learn_more' })}
- </DocumentationLink>
- ),
- }}
- />
- </FlagMessage>
- );
-}
diff --git a/server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx b/server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx
index b418e39d953..9467dc6fedb 100644
--- a/server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx
+++ b/server/sonar-web/src/main/js/components/shared/CleanCodeAttributePill.tsx
@@ -50,7 +50,7 @@ export function CleanCodeAttributePill(props: Readonly<Props>) {
'advice',
)}
footer={
- <DocumentationLink to={DocLink.CleanCodeIntroduction}>
+ <DocumentationLink shouldOpenInNewTab standalone to={DocLink.CleanCodeIntroduction}>
{translate('clean_code_attribute.learn_more')}
</DocumentationLink>
}
diff --git a/server/sonar-web/src/main/js/components/shared/ComponentMissingMqrMetricsMessage.tsx b/server/sonar-web/src/main/js/components/shared/ComponentMissingMqrMetricsMessage.tsx
new file mode 100644
index 00000000000..17e30d7638f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/ComponentMissingMqrMetricsMessage.tsx
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { LinkHighlight } from '@sonarsource/echoes-react';
+import { FormattedMessage } from 'react-intl';
+import { DocLink } from '../../helpers/doc-links';
+import { useCurrentBranchQuery } from '../../queries/branch';
+import { useMeasureQuery } from '../../queries/measures';
+import { useStandardExperienceModeQuery } from '../../queries/mode';
+import { isPullRequest } from '../../sonar-aligned/helpers/branch-like';
+import { LightComponent } from '../../sonar-aligned/types/component';
+import { MetricKey } from '../../sonar-aligned/types/metrics';
+import DocumentationLink from '../common/DocumentationLink';
+import { DismissableAlert } from '../ui/DismissableAlert';
+
+interface AnalysisMissingInfoMessageProps {
+ component: LightComponent;
+}
+
+const ALERT_KEY = 'sonarqube.dismissed_calculation_change_alert.component';
+
+export function ComponentMissingMqrMetricsMessage({
+ component,
+}: Readonly<AnalysisMissingInfoMessageProps>) {
+ const { key: componentKey, qualifier } = component;
+ const { data: isStandardMode, isLoading } = useStandardExperienceModeQuery();
+ const { data: branchLike, isLoading: loadingBranch } = useCurrentBranchQuery(component);
+ const { data: standardMeasure, isLoading: loadingStandardMeasure } = useMeasureQuery(
+ {
+ componentKey,
+ metricKey: MetricKey.security_rating,
+ branchLike,
+ },
+ { enabled: !isLoading && !isStandardMode && !loadingBranch },
+ );
+ const { data: mqrMeasure, isLoading: loadingMQRMeasure } = useMeasureQuery(
+ {
+ componentKey,
+ metricKey: isPullRequest(branchLike)
+ ? MetricKey.new_software_quality_security_rating
+ : MetricKey.software_quality_security_rating,
+ branchLike,
+ },
+ { enabled: !isLoading && !isStandardMode && !loadingBranch && Boolean(standardMeasure) },
+ );
+ const loading = loadingMQRMeasure || loadingStandardMeasure || isLoading || loadingBranch;
+
+ if (loading || isStandardMode || Boolean(mqrMeasure) || !standardMeasure) {
+ return null;
+ }
+
+ return (
+ <DismissableAlert variant="info" alertKey={`${ALERT_KEY}_${componentKey}`}>
+ <FormattedMessage
+ id="overview.missing_project_data"
+ tagName="div"
+ values={{
+ qualifier,
+ link: (text) => (
+ <DocumentationLink
+ shouldOpenInNewTab
+ highlight={LinkHighlight.CurrentColor}
+ className="sw-whitespace-nowrap"
+ to={DocLink.MetricDefinitions}
+ >
+ {text}
+ </DocumentationLink>
+ ),
+ }}
+ />
+ </DismissableAlert>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx b/server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx
index ec6578de8c3..3c7cb8c6657 100644
--- a/server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx
+++ b/server/sonar-web/src/main/js/components/shared/SoftwareImpactPill.tsx
@@ -144,7 +144,7 @@ export default function SoftwareImpactPill(props: Props) {
</>
}
footer={
- <DocumentationLink shouldOpenInNewTab to={DocLink.MQRSeverity}>
+ <DocumentationLink shouldOpenInNewTab standalone to={DocLink.MQRSeverity}>
{translate('severity_impact.help.link')}
</DocumentationLink>
}