diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2024-12-05 15:23:30 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-12-06 20:03:27 +0000 |
commit | 842877a4ac439af372e7ffc9d7448296778f59f8 (patch) | |
tree | be9999ff46b29edf458f447bf35e758b3ce4610e /server/sonar-web/src/main/js | |
parent | 7518acb2cd055589f73b8a05ca53afbf61d4b382 (diff) | |
download | sonarqube-842877a4ac439af372e7ffc9d7448296778f59f8.tar.gz sonarqube-842877a4ac439af372e7ffc9d7448296778f59f8.zip |
SONAR-23850 Improve accessibility for calculation messages
Diffstat (limited to 'server/sonar-web/src/main/js')
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> } |