import { cloneDeep } from 'lodash';
import { OpenAPIV3 } from 'openapi-types';
import { mockAction } from '../../helpers/mocks/webapi';
+import { MetricKey } from '../../sonar-aligned/types/metrics';
import { fetchOpenAPI, fetchWebApi } from '../web-api';
import { openApiTestData } from './data/web-api';
path: 'internal/thing1',
since: '1.3',
},
+ {
+ path: 'api/project_badges',
+ actions: [
+ mockAction({
+ key: 'measure',
+ params: [
+ {
+ key: 'metric',
+ description: 'Badge Metric key',
+ required: true,
+ internal: false,
+ possibleValues: [
+ MetricKey.bugs,
+ MetricKey.software_quality_reliability_issues,
+ MetricKey.code_smells,
+ MetricKey.software_quality_maintainability_issues,
+ MetricKey.vulnerabilities,
+ MetricKey.software_quality_security_issues,
+ MetricKey.sqale_rating,
+ MetricKey.software_quality_maintainability_rating,
+ MetricKey.security_rating,
+ MetricKey.software_quality_security_rating,
+ MetricKey.reliability_rating,
+ MetricKey.software_quality_reliability_rating,
+ MetricKey.coverage,
+ MetricKey.duplicated_lines_density,
+ MetricKey.alert_status,
+ MetricKey.security_hotspots,
+ MetricKey.ncloc,
+ ],
+ },
+ ],
+ }),
+ ],
+ description: 'Badges API',
+ internal: false,
+ since: '1.3',
+ },
];
export default class WebApiServiceMock {
import NotificationsMock from '../../../api/mocks/NotificationsMock';
import { ProjectBadgesServiceMock } from '../../../api/mocks/ProjectBadgesServiceMock';
import ProjectLinksServiceMock from '../../../api/mocks/ProjectLinksServiceMock';
+import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import { mockComponent } from '../../../helpers/mocks/component';
import { mockCurrentUser, mockLoggedInUser, mockMeasure } from '../../../helpers/testMocks';
import {
const notificationsHandler = new NotificationsMock();
const branchesHandler = new BranchesServiceMock();
const aiCodeAssurance = new AiCodeAssuredServiceMock();
+const settingsHandler = new SettingsServiceMock();
const ui = {
projectPageTitle: byRole('heading', { name: 'project.info.title' }),
notificationsHandler.reset();
branchesHandler.reset();
aiCodeAssurance.reset();
+ settingsHandler.reset();
});
it('should show fields for project', async () => {
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { MetricKey } from '~sonar-aligned/types/metrics';
import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { localizeMetric } from '../../../helpers/measures';
+import { translate } from '../../../helpers/l10n';
import {
- DEPRECATED_METRIC_KEYS,
- useBadgeMetricsQuery,
+ useBadgeMetrics,
useBadgeTokenQuery,
useRenewBagdeTokenMutation,
} from '../../../queries/badges';
isLoading: isLoadingToken,
isFetching: isFetchingToken,
} = useBadgeTokenQuery(project);
- const { data: metricOptions, isLoading: isLoadingMetrics } = useBadgeMetricsQuery();
+ const { data: metricOptions, isLoading: isLoadingMetrics } = useBadgeMetrics();
const { mutate: renewToken, isPending: isRenewing } = useRenewBagdeTokenMutation();
const { hasFeature } = useAvailableFeatures();
const isLoading = isLoadingMetrics || isLoadingToken || isRenewing;
</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>
-
- {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>
- )}
- </>
+ <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>
)}
<BasicSeparator className="sw-mb-4" />
--- /dev/null
+/*
+ * 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 { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { MetricKey } from '~sonar-aligned/types/metrics';
+import { ProjectBadgesServiceMock } from '../../../../api/mocks/ProjectBadgesServiceMock';
+import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock';
+import WebApiServiceMock from '../../../../api/mocks/WebApiServiceMock';
+import { getProjectBadgesToken } from '../../../../api/project-badges';
+import { mockBranch } from '../../../../helpers/mocks/branch-like';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { Location } from '../../../../helpers/urls';
+import { byLabelText, byRole, byText } from '../../../../sonar-aligned/helpers/testSelector';
+import { ComponentQualifier } from '../../../../sonar-aligned/types/component';
+import { SettingsKey } from '../../../../types/settings';
+import ProjectBadges, { ProjectBadgesProps } from '../ProjectBadges';
+import { BadgeType } from '../utils';
+
+jest.mock('../../../../helpers/urls', () => ({
+ getHostUrl: () => 'host',
+ getPathUrlAsString: (l: Location) => l.pathname,
+ getProjectUrl: () => ({ pathname: '/dashboard' }) as Location,
+}));
+
+const badgesHandler = new ProjectBadgesServiceMock();
+const webApiHandler = new WebApiServiceMock();
+const settingsHandler = new SettingsServiceMock();
+
+afterEach(() => {
+ badgesHandler.reset();
+ webApiHandler.reset();
+ settingsHandler.reset();
+});
+
+it('should renew token', async () => {
+ const { user, ui } = getPageObjects();
+ jest.mocked(getProjectBadgesToken).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar');
+ renderProjectBadges();
+ await ui.appLoaded();
+
+ expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
+ 'src',
+ expect.stringContaining(
+ `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo`,
+ ),
+ );
+
+ await user.click(screen.getByText('overview.badges.renew'));
+
+ expect(
+ await screen.findByAltText(`overview.badges.${BadgeType.qualityGate}.alt`),
+ ).toHaveAttribute(
+ 'src',
+ expect.stringContaining(
+ 'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar',
+ ),
+ );
+
+ expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
+ 'src',
+ expect.stringContaining(
+ `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=bar`,
+ ),
+ );
+});
+
+it('can select badges in Standard Experience Mode', async () => {
+ const { user, ui } = getPageObjects();
+ settingsHandler.set(SettingsKey.MQRMode, 'false');
+
+ renderProjectBadges();
+ await ui.appLoaded();
+
+ expect(ui.markdownCode(MetricKey.alert_status).get()).toBeInTheDocument();
+
+ await ui.selectMetric(MetricKey.code_smells);
+ expect(ui.markdownCode(MetricKey.code_smells).get()).toBeInTheDocument();
+
+ await ui.selectMetric(MetricKey.security_rating);
+ expect(ui.markdownCode(MetricKey.security_rating).get()).toBeInTheDocument();
+
+ await user.click(ui.imageUrlRadio.get());
+ expect(ui.urlCode(MetricKey.security_rating).get()).toBeInTheDocument();
+
+ await user.click(ui.qualityGateBadge.get());
+ expect(ui.urlCode().get()).toBeInTheDocument();
+
+ await user.click(ui.mardownRadio.get());
+ expect(ui.markdownCode().get()).toBeInTheDocument();
+});
+
+it('can select badges in MQR Mode', async () => {
+ const { user, ui } = getPageObjects();
+
+ renderProjectBadges();
+ await ui.appLoaded();
+
+ expect(ui.markdownCode(MetricKey.alert_status).get()).toBeInTheDocument();
+
+ await ui.selectMetric(MetricKey.coverage);
+ expect(ui.markdownCode(MetricKey.coverage).get()).toBeInTheDocument();
+
+ await ui.selectMetric(MetricKey.software_quality_reliability_issues);
+ expect(ui.markdownCode(MetricKey.software_quality_reliability_issues).get()).toBeInTheDocument();
+
+ await ui.selectMetric(MetricKey.software_quality_maintainability_rating);
+ expect(
+ ui.markdownCode(MetricKey.software_quality_maintainability_rating).get(),
+ ).toBeInTheDocument();
+
+ await user.click(ui.imageUrlRadio.get());
+ expect(ui.urlCode(MetricKey.software_quality_maintainability_rating).get()).toBeInTheDocument();
+
+ await user.click(ui.qualityGateBadge.get());
+ expect(ui.urlCode().get()).toBeInTheDocument();
+
+ await user.click(ui.mardownRadio.get());
+ expect(ui.markdownCode().get()).toBeInTheDocument();
+});
+
+const getPageObjects = () => {
+ const user = userEvent.setup();
+
+ return {
+ user,
+ ui: {
+ qualityGateBadge: byRole('button', {
+ name: `overview.badges.${BadgeType.qualityGate}.alt overview.badges.${BadgeType.qualityGate}.description.${ComponentQualifier.Project}`,
+ }),
+ imageUrlRadio: byRole('radio', { name: 'overview.badges.options.formats.url' }),
+ mardownRadio: byRole('radio', { name: 'overview.badges.options.formats.md' }),
+ urlCode: (metric?: MetricKey) =>
+ byText(
+ metric
+ ? `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${metric}&token=${badgesHandler.token}`
+ : `host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=${badgesHandler.token}`,
+ { exact: false },
+ ),
+ markdownCode: (metric?: MetricKey) =>
+ byText(
+ metric
+ ? `[![${metric}](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${metric}&token=${badgesHandler.token}`
+ : `[![Quality gate](host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=${badgesHandler.token}`,
+ { exact: false },
+ ),
+
+ async selectMetric(metric: MetricKey) {
+ await user.click(byLabelText('overview.badges.metric').get());
+ await user.click(byText(`metric.${metric}.name`).get());
+ },
+ async appLoaded() {
+ await waitFor(() => expect(screen.queryByLabelText(`loading`)).not.toBeInTheDocument());
+ },
+ },
+ };
+};
+
+function renderProjectBadges(props: Partial<ProjectBadgesProps> = {}) {
+ return renderComponent(
+ <ProjectBadges
+ branchLike={mockBranch()}
+ component={mockComponent({ configuration: { showSettings: true } })}
+ {...props}
+ />,
+ );
+}
+++ /dev/null
-/*
- * 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 { screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { ComponentQualifier } from '~sonar-aligned/types/component';
-import { MetricKey } from '~sonar-aligned/types/metrics';
-import { getProjectBadgesToken } from '../../../../api/project-badges';
-import { mockBranch } from '../../../../helpers/mocks/branch-like';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { Location } from '../../../../helpers/urls';
-import { byRole } from '../../../../sonar-aligned/helpers/testSelector';
-import ProjectBadges, { ProjectBadgesProps } from '../ProjectBadges';
-import { BadgeType } from '../utils';
-
-jest.mock('../../../../helpers/urls', () => ({
- getHostUrl: () => 'host',
- getPathUrlAsString: (l: Location) => l.pathname,
- getProjectUrl: () => ({ pathname: '/dashboard' }) as Location,
-}));
-
-jest.mock('../../../../api/project-badges', () => ({
- getProjectBadgesToken: jest.fn().mockResolvedValue('foo'),
- renewProjectBadgesToken: jest.fn().mockResolvedValue({}),
-}));
-
-jest.mock('../../../../api/web-api', () => ({
- fetchWebApi: jest.fn().mockResolvedValue([
- {
- path: 'api/project_badges',
- actions: [
- {
- key: 'measure',
- // eslint-disable-next-line local-rules/use-metrickey-enum
- params: [{ key: 'metric', possibleValues: ['alert_status', 'coverage', 'bugs'] }],
- },
- ],
- },
- ]),
-}));
-
-it('should renew token', async () => {
- const user = userEvent.setup();
- jest.mocked(getProjectBadgesToken).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar');
- renderProjectBadges();
- await appLoaded();
-
- expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
- 'src',
- expect.stringContaining(
- `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo`,
- ),
- );
-
- await user.click(screen.getByText('overview.badges.renew'));
-
- expect(
- await screen.findByAltText(`overview.badges.${BadgeType.qualityGate}.alt`),
- ).toHaveAttribute(
- 'src',
- expect.stringContaining(
- 'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar',
- ),
- );
-
- expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
- 'src',
- expect.stringContaining(
- `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=bar`,
- ),
- );
-});
-
-it('should update params', async () => {
- const user = userEvent.setup();
- renderProjectBadges();
- await appLoaded();
-
- expect(
- screen.getByText(
- `[![${MetricKey.alert_status}](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo`,
- { exact: false },
- ),
- ).toBeInTheDocument();
-
- await user.click(byRole('radio', { name: 'overview.badges.options.formats.url' }).get());
-
- expect(
- screen.getByText(
- 'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo',
- { exact: false },
- ),
- ).toBeInTheDocument();
-
- await user.click(screen.getByLabelText('overview.badges.metric'));
- await user.click(screen.getByText(`metric.${MetricKey.coverage}.name`));
-
- expect(
- screen.getByText(
- 'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=coverage&token=foo',
- { exact: false },
- ),
- ).toBeInTheDocument();
-
- await user.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',
- { exact: false },
- ),
- ).toBeInTheDocument();
-
- await user.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=coverage&token=foo',
- { exact: false },
- ),
- ).toBeInTheDocument();
-});
-
-it('should warn about deprecated metrics', async () => {
- const user = userEvent.setup();
- renderProjectBadges();
- await appLoaded();
-
- await user.click(screen.getByLabelText('overview.badges.metric'));
- await user.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() {
- await waitFor(() => expect(screen.queryByLabelText(`loading`)).not.toBeInTheDocument());
-}
-
-function renderProjectBadges(props: Partial<ProjectBadgesProps> = {}) {
- return renderComponent(
- <ProjectBadges
- branchLike={mockBranch()}
- component={mockComponent({ configuration: { showSettings: true } })}
- {...props}
- />,
- );
-}
renderWebApi();
expect(await ui.sidebarHeader.find()).toBeInTheDocument();
- expect(await ui.domainMenuItems.findAll()).toHaveLength(1);
+ expect(await ui.domainMenuItems.findAll()).toHaveLength(2);
await user.click(ui.domainMenuItemLink('foo/bar').get());
// Show internal
await user.click(ui.showInternalCheckbox.get());
- expect(await ui.domainMenuItems.findAll()).toHaveLength(2);
+ expect(await ui.domainMenuItems.findAll()).toHaveLength(3);
await user.click(ui.domainMenuItemLink('internal/thing1 internal').get());
expect(await byText('get internal memos').find()).toBeInTheDocument();
*/
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { uniq } from 'lodash';
import { MetricKey } from '~sonar-aligned/types/metrics';
import { getProjectBadgesToken, renewProjectBadgesToken } from '../api/project-badges';
-import { translate } from '../helpers/l10n';
+import { MQR_CONDITIONS_MAP, STANDARD_CONDITIONS_MAP } from '../apps/quality-gates/utils';
import { localizeMetric } from '../helpers/measures';
+import { useStandardExperienceMode } from './settings';
import { useWebApiQuery } from './web-api';
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 { data: webservices = [], ...rest } = useWebApiQuery();
+export function useBadgeMetrics() {
+ const { data: webservices = [], isLoading: isLoadingWebApi } = useWebApiQuery();
+ const { data: isStandardExperience, isLoading: isLoadingMode } = useStandardExperienceMode();
const domain = webservices.find((d) => d.path === 'api/project_badges');
const ws = domain?.actions.find((w) => w.key === 'measure');
const param = ws?.params?.find((p) => p.key === 'metric');
- if (param?.possibleValues) {
+ if (param?.possibleValues && !isLoadingMode) {
return {
- ...rest,
- data: param.possibleValues.map((key: MetricKey) => {
- const label = localizeMetric(key);
- return {
- value: key,
- label: DEPRECATED_METRIC_KEYS.includes(key)
- ? `${label} (${translate('deprecated')})`
- : label,
- };
- }),
+ isLoading: false,
+ data: uniq(
+ param.possibleValues.map((metric: MetricKey) => {
+ return (
+ (isStandardExperience ? MQR_CONDITIONS_MAP[metric] : STANDARD_CONDITIONS_MAP[metric]) ??
+ metric
+ );
+ }),
+ ).map((metric) => ({
+ value: metric,
+ label: localizeMetric(metric),
+ })),
};
}
- return { ...rest, data: [] };
+ return { isLoading: isLoadingWebApi || isLoadingMode, data: [] };
}
export function useBadgeTokenQuery(componentKey: string) {
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 {productName} {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