@@ -33,8 +33,10 @@ import { isEmpty } from 'lodash'; | |||
import * as React from 'react'; | |||
import { useState } from 'react'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { localizeMetric } from '../../../helpers/measures'; | |||
import { | |||
DEPRECATED_METRIC_KEYS, | |||
useBadgeMetricsQuery, | |||
useBadgeTokenQuery, | |||
useRenewBagdeTokenMutation, | |||
@@ -130,19 +132,31 @@ export default function ProjectBadges(props: ProjectBadgesProps) { | |||
</Spinner> | |||
{BadgeType.measure === selectedType && ( | |||
<FormField htmlFor="badge-param-customize" label={translate('overview.badges.metric')}> | |||
<InputSelect | |||
className="sw-w-abs-300" | |||
inputId="badge-param-customize" | |||
options={metricOptions} | |||
onChange={(option) => { | |||
if (option) { | |||
setSelectedMetric(option.value); | |||
} | |||
}} | |||
value={metricOptions.find((m) => m.value === selectedMetric)} | |||
/> | |||
</FormField> | |||
<> | |||
<FormField htmlFor="badge-param-customize" label={translate('overview.badges.metric')}> | |||
<InputSelect | |||
className="sw-w-abs-300" | |||
inputId="badge-param-customize" | |||
options={metricOptions} | |||
onChange={(option) => { | |||
if (option) { | |||
setSelectedMetric(option.value); | |||
} | |||
}} | |||
value={metricOptions.find((m) => m.value === selectedMetric)} | |||
/> | |||
</FormField> | |||
{DEPRECATED_METRIC_KEYS.includes(selectedMetric) && ( | |||
<FlagMessage className="sw-mb-4" variant="warning"> | |||
{translateWithParameters( | |||
'overview.badges.deprecated_badge_x_y', | |||
localizeMetric(selectedMetric), | |||
translate('qualifier', qualifier), | |||
)} | |||
</FlagMessage> | |||
)} | |||
</> | |||
)} | |||
<BasicSeparator className="sw-mb-4" /> |
@@ -26,6 +26,7 @@ import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { Location } from '../../../../helpers/urls'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import ProjectBadges, { ProjectBadgesProps } from '../ProjectBadges'; | |||
import { BadgeType } from '../utils'; | |||
@@ -49,7 +50,7 @@ jest.mock('../../../../api/web-api', () => ({ | |||
{ | |||
key: 'measure', | |||
// eslint-disable-next-line local-rules/use-metrickey-enum | |||
params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage'] }], | |||
params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage', 'bugs'] }], | |||
}, | |||
], | |||
}, | |||
@@ -108,13 +109,53 @@ it('should update params', async () => { | |||
await act(async () => { | |||
await selectEvent.openMenu(screen.getByLabelText('overview.badges.metric')); | |||
}); | |||
fireEvent.click(screen.getByText(MetricKey.coverage)); | |||
fireEvent.click(screen.getByText(`metric.${MetricKey.coverage}.name`)); | |||
expect( | |||
screen.getByText( | |||
`host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo`, | |||
), | |||
).toBeInTheDocument(); | |||
fireEvent.click( | |||
screen.getByRole('button', { | |||
name: `overview.badges.${BadgeType.qualityGate}.alt overview.badges.${BadgeType.qualityGate}.description.${ComponentQualifier.Project}`, | |||
}), | |||
); | |||
expect( | |||
screen.getByText( | |||
`host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo`, | |||
), | |||
).toBeInTheDocument(); | |||
fireEvent.click( | |||
screen.getByRole('button', { | |||
name: `overview.badges.${BadgeType.measure}.alt overview.badges.${BadgeType.measure}.description.${ComponentQualifier.Project}`, | |||
}), | |||
); | |||
expect( | |||
screen.getByText( | |||
`host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo`, | |||
), | |||
).toBeInTheDocument(); | |||
}); | |||
it('should warn about deprecated metrics', async () => { | |||
renderProjectBadges(); | |||
await appLoaded(); | |||
await act(async () => { | |||
await selectEvent.openMenu(screen.getByLabelText('overview.badges.metric')); | |||
}); | |||
fireEvent.click(screen.getByText(`metric.${MetricKey.bugs}.name (deprecated)`)); | |||
expect( | |||
screen.getByText( | |||
`overview.badges.deprecated_badge_x_y.metric.${MetricKey.bugs}.name.qualifier.${ComponentQualifier.Project}`, | |||
), | |||
).toBeInTheDocument(); | |||
}); | |||
async function appLoaded() { |
@@ -19,10 +19,9 @@ | |||
*/ | |||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | |||
import { useContext } from 'react'; | |||
import { getProjectBadgesToken, renewProjectBadgesToken } from '../api/project-badges'; | |||
import { MetricsContext } from '../app/components/metrics/MetricsContext'; | |||
import { getLocalizedMetricName } from '../helpers/l10n'; | |||
import { translate } from '../helpers/l10n'; | |||
import { localizeMetric } from '../helpers/measures'; | |||
import { MetricKey } from '../types/metrics'; | |||
import { useWebApiQuery } from './web-api'; | |||
@@ -38,8 +37,15 @@ export function useRenewBagdeTokenMutation() { | |||
}); | |||
} | |||
// The same list of deprecated metric keys is maintained on the backend at org.sonar.server.badge.ws.MeasureAction. | |||
export const DEPRECATED_METRIC_KEYS = [ | |||
MetricKey.bugs, | |||
MetricKey.code_smells, | |||
MetricKey.security_hotspots, | |||
MetricKey.vulnerabilities, | |||
]; | |||
export function useBadgeMetricsQuery() { | |||
const metrics = useContext(MetricsContext); | |||
const { data: webservices = [], ...rest } = useWebApiQuery(); | |||
const domain = webservices.find((d) => d.path === 'api/project_badges'); | |||
const ws = domain?.actions.find((w) => w.key === 'measure'); | |||
@@ -47,11 +53,13 @@ export function useBadgeMetricsQuery() { | |||
if (param?.possibleValues) { | |||
return { | |||
...rest, | |||
data: param.possibleValues.map((key) => { | |||
const metric = metrics[key]; | |||
data: param.possibleValues.map((key: MetricKey) => { | |||
const label = localizeMetric(key); | |||
return { | |||
value: key as MetricKey, | |||
label: metric ? getLocalizedMetricName(metric) : key, | |||
value: key, | |||
label: DEPRECATED_METRIC_KEYS.includes(key) | |||
? `${label} (${translate('deprecated')})` | |||
: label, | |||
}; | |||
}), | |||
}; |
@@ -28,6 +28,7 @@ import java.util.Locale; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.function.Function; | |||
import org.sonar.api.server.ws.Change; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
@@ -94,6 +95,8 @@ public class MeasureAction implements ProjectBadgesWsAction { | |||
.put(VULNERABILITIES_KEY, "vulnerabilities") | |||
.build(); | |||
private static final String[] DEPRECATED_METRIC_KEYS = {BUGS_KEY, CODE_SMELLS_KEY, SECURITY_HOTSPOTS_KEY, VULNERABILITIES_KEY}; | |||
private static final Map<Level, String> QUALITY_GATE_MESSAGE_BY_STATUS = new EnumMap<>(Map.of( | |||
OK, "passed", | |||
ERROR, "failed")); | |||
@@ -126,6 +129,7 @@ public class MeasureAction implements ProjectBadgesWsAction { | |||
.setDescription("Generate badge for project's measure as an SVG.<br/>" + | |||
"Requires 'Browse' permission on the specified project.") | |||
.setSince("7.1") | |||
.setChangelog(new Change("10.4", String.format("The following metric keys are now deprecated: %s", String.join(", ", DEPRECATED_METRIC_KEYS)))) | |||
.setResponseExample(Resources.getResource(getClass(), "measure-example.svg")); | |||
support.addProjectAndBranchParams(action); | |||
action.createParam(PARAM_METRIC) |
@@ -4001,6 +4001,7 @@ overview.badges.quality_gate.description.VW=Displays the current quality gate st | |||
overview.badges.leak_warning=Project badges can expose your security rating and other measures. Only use project badges in trusted environments. | |||
overview.badges.renew=Renew Token | |||
overview.badges.renew.description=If your project badge security token has leaked to an unsafe environment, you can renew it: | |||
overview.badges.deprecated_badge_x_y=Badges displaying {0} are deprecated and will be removed in a future version. Please choose another badge for your {1}. | |||
overview.quality_profiles_update_after_sq_upgrade.message=Upgrade to SonarQube {sqVersion} has updated your Quality Profiles. Issues on your project may have been affected. {link} | |||
overview.quality_profiles_update_after_sq_upgrade.link=See more details |