: 'issue.type.SECURITY_HOTSPOT.plural' | : 'issue.type.SECURITY_HOTSPOT.plural' | ||||
} | } | ||||
url={getComponentSecurityHotspotsUrl(component.key, { | url={getComponentSecurityHotspotsUrl(component.key, { | ||||
inNewCodePeriod: 'true', | |||||
...getBranchLikeQuery(branch), | ...getBranchLikeQuery(branch), | ||||
})} | })} | ||||
value={newSecurityHotspots} | value={newSecurityHotspots} |
import { Component, MeasureEnhanced } from '../../../types/types'; | import { Component, MeasureEnhanced } from '../../../types/types'; | ||||
import MeasuresCard from '../components/MeasuresCard'; | import MeasuresCard from '../components/MeasuresCard'; | ||||
import MeasuresCardNumber from '../components/MeasuresCardNumber'; | import MeasuresCardNumber from '../components/MeasuresCardNumber'; | ||||
import { OverviewDisabledLinkTooltip } from '../components/OverviewDisabledLinkTooltip'; | |||||
import MeasuresPanelPercentCards from './MeasuresPanelPercentCards'; | import MeasuresPanelPercentCards from './MeasuresPanelPercentCards'; | ||||
import SoftwareImpactMeasureCard from './SoftwareImpactMeasureCard'; | import SoftwareImpactMeasureCard from './SoftwareImpactMeasureCard'; | ||||
color={acceptedIssues === '0' ? 'overviewCardDefaultIcon' : 'overviewCardWarningIcon'} | color={acceptedIssues === '0' ? 'overviewCardDefaultIcon' : 'overviewCardWarningIcon'} | ||||
/> | /> | ||||
} | } | ||||
disabled={component.needIssueSync} | |||||
tooltip={component.needIssueSync ? <OverviewDisabledLinkTooltip /> : null} | |||||
> | > | ||||
<TextSubdued className="sw-body-xs sw-mt-3"> | <TextSubdued className="sw-body-xs sw-mt-3"> | ||||
{intl.formatMessage({ | {intl.formatMessage({ |
*/ | */ | ||||
import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||
import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react'; | import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react'; | ||||
import classNames from 'classnames'; | |||||
import { Badge, LightGreyCard, LightGreyCardTitle, TextBold, TextSubdued } from 'design-system'; | import { Badge, LightGreyCard, LightGreyCardTitle, TextBold, TextSubdued } from 'design-system'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { FormattedMessage, useIntl } from 'react-intl'; | import { FormattedMessage, useIntl } from 'react-intl'; | ||||
import { MetricKey, MetricType } from '../../../types/metrics'; | import { MetricKey, MetricType } from '../../../types/metrics'; | ||||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | ||||
import { Component, MeasureEnhanced } from '../../../types/types'; | import { Component, MeasureEnhanced } from '../../../types/types'; | ||||
import { OverviewDisabledLinkTooltip } from '../components/OverviewDisabledLinkTooltip'; | |||||
import { Status, softwareQualityToMeasure } from '../utils'; | import { Status, softwareQualityToMeasure } from '../utils'; | ||||
import SoftwareImpactMeasureBreakdownCard from './SoftwareImpactMeasureBreakdownCard'; | import SoftwareImpactMeasureBreakdownCard from './SoftwareImpactMeasureBreakdownCard'; | ||||
import SoftwareImpactMeasureRating from './SoftwareImpactMeasureRating'; | import SoftwareImpactMeasureRating from './SoftwareImpactMeasureRating'; | ||||
(severity) => measure[severity] > 0, | (severity) => measure[severity] > 0, | ||||
); | ); | ||||
const countTooltipOverlay = component.needIssueSync ? ( | |||||
<OverviewDisabledLinkTooltip /> | |||||
) : ( | |||||
intl.formatMessage({ id: 'overview.measures.software_impact.count_tooltip' }) | |||||
); | |||||
const countTooltipOverlay = intl.formatMessage({ | |||||
id: 'overview.measures.software_impact.count_tooltip', | |||||
}); | |||||
const failed = conditions.some((c) => c.level === Status.ERROR && c.metric === ratingMetricKey); | const failed = conditions.some((c) => c.level === Status.ERROR && c.metric === ratingMetricKey); | ||||
</LightGreyCardTitle> | </LightGreyCardTitle> | ||||
<div className="sw-flex sw-flex-col sw-gap-3"> | <div className="sw-flex sw-flex-col sw-gap-3"> | ||||
<div className="sw-flex sw-mt-4"> | <div className="sw-flex sw-mt-4"> | ||||
<div | |||||
className={classNames('sw-flex sw-gap-1 sw-items-center', { | |||||
'sw-opacity-60': component.needIssueSync, | |||||
})} | |||||
> | |||||
<div className="sw-flex sw-gap-1 sw-items-center"> | |||||
{count ? ( | {count ? ( | ||||
<Tooltip overlay={countTooltipOverlay}> | <Tooltip overlay={countTooltipOverlay}> | ||||
<LinkStandalone | <LinkStandalone |
expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(); | expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(); | ||||
expect(screen.getByText(/overview.X_conditions_failed/)).toBeInTheDocument(); | expect(screen.getByText(/overview.X_conditions_failed/)).toBeInTheDocument(); | ||||
expect(screen.getAllByText(/overview.quality_gate.required_x/)).toHaveLength(3); | expect(screen.getAllByText(/overview.quality_gate.required_x/)).toHaveLength(3); | ||||
expect( | |||||
screen.getByRole('link', { | |||||
name: '1 1 metric.new_security_hotspots_reviewed.name quality_gates.operator.GT 2', | |||||
}), | |||||
).toHaveAttribute('href', '/security_hotspots?id=foo&inNewCodePeriod=true'); | |||||
}); | }); | ||||
it('should correctly show a project as empty', async () => { | it('should correctly show a project as empty', async () => { | ||||
expect(await screen.findByText('overview.missing_project_data.TRK')).toBeInTheDocument(); | expect(await screen.findByText('overview.missing_project_data.TRK')).toBeInTheDocument(); | ||||
}, | }, | ||||
); | ); | ||||
it('should disable software impact measure card links during reindexing', async () => { | |||||
const { user, ui } = getPageObjects(); | |||||
renderBranchOverview({ | |||||
component: mockComponent({ | |||||
breadcrumbs: [mockComponent({ key: 'foo' })], | |||||
key: 'foo', | |||||
needIssueSync: true, | |||||
}), | |||||
}); | |||||
await user.click(await ui.overallCodeButton.find()); | |||||
expect(await ui.softwareImpactMeasureCard(SoftwareQuality.Security).find()).toBeInTheDocument(); | |||||
ui.expectSoftwareImpactMeasureCard( | |||||
SoftwareQuality.Security, | |||||
'B', | |||||
{ | |||||
total: 1, | |||||
[SoftwareImpactSeverity.High]: 0, | |||||
[SoftwareImpactSeverity.Medium]: 1, | |||||
[SoftwareImpactSeverity.Low]: 0, | |||||
}, | |||||
[false, true, false], | |||||
); | |||||
await expect( | |||||
byRole('link', { | |||||
name: `overview.measures.software_impact.see_list_of_x_open_issues.${1}.software_quality.${SoftwareQuality.Security}`, | |||||
}).get(), | |||||
).toHaveATooltipWithContent('indexation.in_progress'); | |||||
}); | |||||
}); | }); | ||||
describe('application overview', () => { | describe('application overview', () => { |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||
import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react'; | |||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import { Badge, ContentLink, NoDataIcon, themeColor } from 'design-system'; | |||||
import { Badge, NoDataIcon, themeColor } from 'design-system'; | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { Path } from 'react-router-dom'; | import { Path } from 'react-router-dom'; | ||||
import Tooltip from '../../../components/controls/Tooltip'; | |||||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | import { translate, translateWithParameters } from '../../../helpers/l10n'; | ||||
import { localizeMetric } from '../../../helpers/measures'; | import { localizeMetric } from '../../../helpers/measures'; | ||||
import { MetricKey } from '../../../types/metrics'; | import { MetricKey } from '../../../types/metrics'; | ||||
import { OverviewDisabledLinkTooltip } from './OverviewDisabledLinkTooltip'; | |||||
interface IssueMeasuresCardInnerProps extends React.HTMLAttributes<HTMLDivElement> { | interface IssueMeasuresCardInnerProps extends React.HTMLAttributes<HTMLDivElement> { | ||||
metric: MetricKey; | metric: MetricKey; | ||||
</ColorBold> | </ColorBold> | ||||
<div className="sw-flex sw-justify-between sw-items-center sw-h-9"> | <div className="sw-flex sw-justify-between sw-items-center sw-h-9"> | ||||
<div className="sw-h-fit"> | <div className="sw-h-fit"> | ||||
<Tooltip | |||||
classNameSpace={disabled ? 'tooltip' : 'sw-hidden'} | |||||
overlay={value && <OverviewDisabledLinkTooltip />} | |||||
<LinkStandalone | |||||
highlight={LinkHighlight.Default} | |||||
aria-label={ | |||||
value | |||||
? translateWithParameters( | |||||
'overview.see_more_details_on_x_of_y', | |||||
value, | |||||
localizeMetric(metric), | |||||
) | |||||
: translateWithParameters('no_measure_value_x', localizeMetric(metric)) | |||||
} | |||||
className="it__overview-measures-value sw-w-fit sw-text-lg" | |||||
to={url} | |||||
> | > | ||||
<ContentLink | |||||
disabled={disabled || !value} | |||||
aria-label={ | |||||
value | |||||
? translateWithParameters( | |||||
'overview.see_more_details_on_x_of_y', | |||||
value, | |||||
localizeMetric(metric), | |||||
) | |||||
: translate('no_data') | |||||
} | |||||
className="it__overview-measures-value sw-w-fit sw-text-lg" | |||||
to={url} | |||||
> | |||||
{value ? value : '-'} | |||||
</ContentLink> | |||||
</Tooltip> | |||||
{value ?? '-'} | |||||
</LinkStandalone> | |||||
</div> | </div> | ||||
{value ? icon : <NoDataIcon size="md" />} | {value ? icon : <NoDataIcon size="md" />} | ||||
</div> | </div> |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||
import { Badge, Card, ContentLink, Tooltip, themeBorder, themeColor } from 'design-system'; | |||||
import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react'; | |||||
import { Badge, Card, themeBorder, themeColor } from 'design-system'; | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { To } from 'react-router-dom'; | import { To } from 'react-router-dom'; | ||||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | import { translate, translateWithParameters } from '../../../helpers/l10n'; | ||||
label: string; | label: string; | ||||
failed?: boolean; | failed?: boolean; | ||||
icon?: React.ReactElement; | icon?: React.ReactElement; | ||||
disabled?: boolean; | |||||
tooltip?: React.ReactNode | null; | |||||
} | } | ||||
export default function MeasuresCard( | export default function MeasuresCard( | ||||
props: React.PropsWithChildren<MeasuresCardProps & React.HTMLAttributes<HTMLDivElement>>, | props: React.PropsWithChildren<MeasuresCardProps & React.HTMLAttributes<HTMLDivElement>>, | ||||
) { | ) { | ||||
const { failed, children, metric, icon, value, url, label, disabled, tooltip, ...rest } = props; | |||||
const { failed, children, metric, icon, value, url, label, ...rest } = props; | |||||
return ( | return ( | ||||
<StyledCard className="sw-p-6 sw-rounded-2 sw-text-base" {...rest}> | <StyledCard className="sw-p-6 sw-rounded-2 sw-text-base" {...rest}> | ||||
</Badge> | </Badge> | ||||
)} | )} | ||||
<div className="sw-flex sw-items-center sw-mt-1 sw-justify-between sw-font-semibold"> | <div className="sw-flex sw-items-center sw-mt-1 sw-justify-between sw-font-semibold"> | ||||
{value ? ( | |||||
<Tooltip overlay={tooltip}> | |||||
<ContentLink | |||||
aria-label={translateWithParameters( | |||||
'overview.see_more_details_on_x_of_y', | |||||
value, | |||||
localizeMetric(metric), | |||||
)} | |||||
className="it__overview-measures-value sw-text-lg" | |||||
to={url} | |||||
disabled={disabled} | |||||
> | |||||
{value} | |||||
</ContentLink> | |||||
</Tooltip> | |||||
) : ( | |||||
<ColorBold> — </ColorBold> | |||||
)} | |||||
<LinkStandalone | |||||
highlight={LinkHighlight.Default} | |||||
aria-label={ | |||||
value | |||||
? translateWithParameters( | |||||
'overview.see_more_details_on_x_of_y', | |||||
value, | |||||
localizeMetric(metric), | |||||
) | |||||
: translateWithParameters('no_measure_value_x', localizeMetric(metric)) | |||||
} | |||||
className="it__overview-measures-value sw-text-lg" | |||||
to={url} | |||||
> | |||||
{value ?? '-'} | |||||
</LinkStandalone> | |||||
{icon} | {icon} | ||||
</div> | </div> | ||||
{children && <div className="sw-flex sw-flex-col">{children}</div>} | {children && <div className="sw-flex sw-flex-col">{children}</div>} |
/* | |||||
* 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 * as React from 'react'; | |||||
import DocumentationLink from '../../../components/common/DocumentationLink'; | |||||
import { translate } from '../../../helpers/l10n'; | |||||
export function OverviewDisabledLinkTooltip() { | |||||
return ( | |||||
<div className="sw-body-sm sw-w-[280px]"> | |||||
{translate('indexation.in_progress')} | |||||
<br /> | |||||
{translate('indexation.link_unavailable')} | |||||
<hr className="sw-mx-0 sw-my-3 sw-p-0 sw-w-full" /> | |||||
<span className="sw-body-sm-highlight">{translate('indexation.learn_more')}</span> | |||||
<DocumentationLink className="sw-ml-1" to="/instance-administration/reindexing/"> | |||||
{translate('indexation.reindexing')} | |||||
</DocumentationLink> | |||||
</div> | |||||
); | |||||
} |
}).get(), | }).get(), | ||||
).toHaveAttribute('href', '/project/issues?fixedInPullRequest=1001&id=foo'); | ).toHaveAttribute('href', '/project/issues?fixedInPullRequest=1001&id=foo'); | ||||
expect(screen.getByLabelText('no_data')).toBeInTheDocument(); | |||||
expect( | |||||
screen.getByRole('link', { name: 'no_measure_value_x.metric.new_security_hotspots.name' }), | |||||
).toBeInTheDocument(); | |||||
expect( | |||||
screen.getByRole('link', { name: 'no_measure_value_x.metric.new_accepted_issues.name' }), | |||||
).toBeInTheDocument(); | |||||
expect( | |||||
screen.getByRole('link', { | |||||
name: 'no_measure_value_x.metric.new_duplicated_lines_density.name', | |||||
}), | |||||
).toBeInTheDocument(); | |||||
}); | }); | ||||
it('should render correctly for a passed QG', async () => { | it('should render correctly for a passed QG', async () => { |
new_violations=New violations | new_violations=New violations | ||||
new_window=New window | new_window=New window | ||||
no_data=No data | no_data=No data | ||||
no_measure_value_x=No measure value for {0} | |||||
no_results=No results | no_results=No results | ||||
no_results_for_x=No results for "{0}" | no_results_for_x=No results for "{0}" | ||||
no_results_search=We couldn't find any results matching selected criteria. | no_results_search=We couldn't find any results matching selected criteria. | ||||
#------------------------------------------------------------------------------ | #------------------------------------------------------------------------------ | ||||
indexation.in_progress=Reindexing in progress. | indexation.in_progress=Reindexing in progress. | ||||
indexation.details_unavailable=Details are unavailable until this process is complete. | indexation.details_unavailable=Details are unavailable until this process is complete. | ||||
indexation.link_unavailable=The link to these results is unavailable until this process is complete. | |||||
indexation.features_partly_available=Most features are available. Some details only show upon completion. {link} | indexation.features_partly_available=Most features are available. Some details only show upon completion. {link} | ||||
indexation.features_partly_available.link=More info | indexation.features_partly_available.link=More info | ||||
indexation.progression={0} out of {1} projects reindexed. | indexation.progression={0} out of {1} projects reindexed. |