import userEvent from '@testing-library/user-event';
import { byRole, byText } from '~sonar-aligned/helpers/testSelector';
import QualityProfilesServiceMock from '../../../api/mocks/QualityProfilesServiceMock';
+import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
+import { SettingsKey } from '../../../types/settings';
import routes from '../routes';
jest.mock('../../../api/quality-profiles');
beforeEach(() => {
serviceMock.reset();
+ settingsMock.reset();
});
const serviceMock = new QualityProfilesServiceMock();
+const settingsMock = new SettingsServiceMock();
const ui = {
loading: byRole('status', { name: 'loading' }),
permissionSection: byRole('region', { name: 'permissions.page' }),
projectSection: byRole('region', { name: 'projects' }),
rulesSection: byRole('region', { name: 'rules' }),
+ rulesSectionHeader: byRole('heading', { name: 'quality_profile.rules.breakdown' }),
exportersSection: byRole('region', { name: 'quality_profiles.exporters' }),
inheritanceSection: byRole('region', { name: 'quality_profiles.profile_inheritance' }),
grantPermissionButton: byRole('button', {
}),
qualityProfilesHeader: byRole('heading', { name: 'quality_profiles.page' }),
deleteQualityProfileButton: byRole('menuitem', { name: 'delete' }),
- activateMoreRulesButton: byRole('button', { name: 'quality_profiles.activate_more' }),
activateMoreLink: byRole('link', { name: 'quality_profiles.activate_more' }),
+ activateMoreButton: byRole('button', { name: 'quality_profiles.activate_more' }),
activateMoreRulesLink: byRole('menuitem', { name: 'quality_profiles.activate_more_rules' }),
backUpLink: byRole('menuitem', { name: 'backup_verb open_in_new_tab' }),
compareLink: byRole('menuitem', { name: 'compare' }),
expect(await ui.rulesSection.find()).toBeInTheDocument();
- expect(ui.activateMoreLink.get()).toBeInTheDocument();
+ expect(await ui.activateMoreLink.find()).toBeInTheDocument();
expect(ui.activateMoreLink.get()).toHaveAttribute(
'href',
'/coding_rules?qprofile=old-php-qp&activation=false',
renderQualityProfile('sonar');
await ui.waitForDataLoaded();
expect(await ui.rulesSection.find()).toBeInTheDocument();
- expect(ui.activateMoreRulesButton.get()).toBeInTheDocument();
- expect(ui.activateMoreRulesButton.get()).toBeDisabled();
+ expect(await ui.activateMoreButton.find()).toBeInTheDocument();
+ expect(ui.activateMoreButton.get()).toBeDisabled();
});
});
renderQualityProfile();
await ui.waitForDataLoaded();
- expect(await ui.rulesSection.find()).toBeInTheDocument();
+ expect(await ui.rulesSectionHeader.find()).toBeInTheDocument();
ui.checkRuleRow('rule.clean_code_attribute_category.INTENTIONAL', 23, 4);
ui.checkRuleRow('rule.clean_code_attribute_category.CONSISTENT', 2, 18);
ui.checkRuleRow('software_quality.SECURITY', 0, 14);
});
+ it('should be able to see active/inactive rules for a Quality Profile in Legacy mode', async () => {
+ settingsMock.set(SettingsKey.MQRMode, 'false');
+ renderQualityProfile();
+ await ui.waitForDataLoaded();
+
+ expect(await ui.rulesSectionHeader.find()).toBeInTheDocument();
+
+ ui.checkRuleRow('issue.type.BUG.plural', 60, 0);
+ ui.checkRuleRow('issue.type.VULNERABILITY.plural', 40, 0);
+ ui.checkRuleRow('issue.type.CODE_SMELL.plural', 250, 0);
+ ui.checkRuleRow('issue.type.SECURITY_HOTSPOT.plural', 50, 0);
+ });
+
it('should be able to see a warning when some rules are missing compare to Sonar way', async () => {
renderQualityProfile();
await ui.waitForDataLoaded();
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
+import { Heading, Spinner } from '@sonarsource/echoes-react';
import {
ButtonPrimary,
ContentCell,
NumericalCell,
- Spinner,
- SubTitle,
Table,
TableRow,
themeColor,
} from 'design-system';
import { keyBy } from 'lodash';
import * as React from 'react';
-import { useEffect, useState } from 'react';
import DocHelpTooltip from '~sonar-aligned/components/controls/DocHelpTooltip';
-import { getQualityProfile } from '../../../api/quality-profiles';
-import { searchRules } from '../../../api/rules';
import { translate } from '../../../helpers/l10n';
import { isDefined } from '../../../helpers/types';
import { getRulesUrl } from '../../../helpers/urls';
+import { useGetQualityProfile } from '../../../queries/quality-profiles';
+import { useSearchRulesQuery } from '../../../queries/rules';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
import { CleanCodeAttributeCategory, SoftwareQuality } from '../../../types/clean-code-taxonomy';
import { SearchRulesResponse } from '../../../types/coding-rules';
import { RulesFacetName } from '../../../types/rules';
-import { Dict } from '../../../types/types';
+import { RuleTypes } from '../../../types/types';
import { Profile } from '../types';
import ProfileRulesDeprecatedWarning from './ProfileRulesDeprecatedWarning';
import ProfileRulesRow from './ProfileRulesRow';
}
export default function ProfileRules({ profile }: Readonly<Props>) {
+ const { data: isLegacy } = useIsLegacyCCTMode();
const activateMoreUrl = getRulesUrl({ qprofile: profile.key, activation: 'false' });
const { actions = {} } = profile;
- const [loading, setLoading] = useState(false);
- const [countsByCctCategory, setCountsByCctCategory] = useState<Dict<ByType>>({});
- const [totalByCctCategory, setTotalByCctCategory] = useState<Dict<ByType>>({});
- const [countsBySoftwareImpact, setCountsBySoftwareImpact] = useState<Dict<ByType>>({});
- const [totalBySoftwareQuality, setTotalBySoftwareQuality] = useState<Dict<ByType>>({});
- const [sonarWayDiff, setSonarWayDiff] = useState<{
- missingRuleCount: number;
- profile: string;
- profileName: string;
- } | null>(null);
-
- useEffect(() => {
- async function loadRules() {
- function findFacet(response: SearchRulesResponse, property: string) {
- const facet = response.facets?.find((f) => f.property === property);
- return facet ? facet.values : [];
- }
+ const { data: allRules, isLoading: isAllRulesLoading } = useSearchRulesQuery({
+ ps: 1,
+ languages: profile.language,
+ facets: isLegacy
+ ? `${RulesFacetName.Types}`
+ : `${RulesFacetName.CleanCodeAttributeCategories},${RulesFacetName.ImpactSoftwareQualities}`,
+ });
+
+ const { data: activatedRules, isLoading: isActivatedRulesLoading } = useSearchRulesQuery(
+ {
+ ps: 1,
+ activation: 'true',
+ facets: isLegacy
+ ? `${RulesFacetName.Types}`
+ : `${RulesFacetName.CleanCodeAttributeCategories},${RulesFacetName.ImpactSoftwareQualities}`,
+ qprofile: profile.key,
+ },
+ { enabled: !!allRules },
+ );
+
+ const { data: sonarWayDiff, isLoading: isShowProfileLoading } = useGetQualityProfile(
+ { compareToSonarWay: true, profile },
+ { enabled: !profile.isBuiltIn, select: (data) => data.compareToSonarWay },
+ );
+
+ const findFacet = React.useCallback((response: SearchRulesResponse, property: string) => {
+ const facet = response.facets?.find((f) => f.property === property);
+ return facet ? facet.values : [];
+ }, []);
- try {
- setLoading(true);
- return await Promise.all([
- searchRules({
- languages: profile.language,
- facets: `${RulesFacetName.CleanCodeAttributeCategories},${RulesFacetName.ImpactSoftwareQualities}`,
- }),
- searchRules({
- activation: 'true',
- facets: `${RulesFacetName.CleanCodeAttributeCategories},${RulesFacetName.ImpactSoftwareQualities}`,
- qprofile: profile.key,
- }),
- !profile.isBuiltIn &&
- getQualityProfile({
- compareToSonarWay: true,
- profile,
- }),
- ]).then((responses) => {
- const [allRules, activatedRules, showProfile] = responses;
- const extractFacetData = (facetName: string, response: SearchRulesResponse) => {
- return keyBy<ByType>(findFacet(response, facetName), 'val');
- };
- setTotalByCctCategory(
- extractFacetData(RulesFacetName.CleanCodeAttributeCategories, allRules),
- );
- setCountsByCctCategory(
- extractFacetData(RulesFacetName.CleanCodeAttributeCategories, activatedRules),
- );
- setTotalBySoftwareQuality(
- extractFacetData(RulesFacetName.ImpactSoftwareQualities, allRules),
- );
- setCountsBySoftwareImpact(
- extractFacetData(RulesFacetName.ImpactSoftwareQualities, activatedRules),
- );
- setSonarWayDiff(showProfile?.compareToSonarWay);
- });
- } finally {
- setLoading(false);
+ const extractFacetData = React.useCallback(
+ (facetName: string, response: SearchRulesResponse | null | undefined) => {
+ if (!response) {
+ return {};
}
- }
- loadRules();
- }, [profile]);
+ return keyBy<ByType>(findFacet(response, facetName), 'val');
+ },
+ [findFacet],
+ );
- if (loading) {
- return <Spinner />;
- }
+ const totalByCctCategory = extractFacetData(
+ RulesFacetName.CleanCodeAttributeCategories,
+ allRules,
+ );
+ const countsByCctCategory = extractFacetData(
+ RulesFacetName.CleanCodeAttributeCategories,
+ activatedRules,
+ );
+ const totalBySoftwareQuality = extractFacetData(RulesFacetName.ImpactSoftwareQualities, allRules);
+ const countsBySoftwareImpact = extractFacetData(
+ RulesFacetName.ImpactSoftwareQualities,
+ activatedRules,
+ );
+ const totalByTypes = extractFacetData(RulesFacetName.Types, allRules);
+ const countsByTypes = extractFacetData(RulesFacetName.Types, activatedRules);
return (
<section aria-label={translate('rules')} className="it__quality-profiles__rules">
- <SubTitle>{translate('quality_profile.rules.breakdown')}</SubTitle>
-
- <Table
- columnCount={3}
- columnWidths={['50%', '25%', '25%']}
- header={
- <StyledTableRowHeader>
- <ContentCell className="sw-font-semibold sw-pl-4">
- {translate('quality_profile.rules.cct_categories_title')}
- </ContentCell>
- <NumericalCell className="sw-font-regular">{translate('active')}</NumericalCell>
- <NumericalCell className="sw-pr-4 sw-font-regular">
- {translate('inactive')}
- </NumericalCell>
- </StyledTableRowHeader>
- }
- noHeaderTopBorder
- noSidePadding
- withRoundedBorder
- >
- {Object.values(CleanCodeAttributeCategory).map((category) => (
- <ProfileRulesRow
- title={translate('rule.clean_code_attribute_category', category)}
- total={totalByCctCategory[category]?.count}
- count={countsByCctCategory[category]?.count}
- key={category}
- qprofile={profile.key}
- propertyName={RulesFacetName.CleanCodeAttributeCategories}
- propertyValue={category}
- />
- ))}
- </Table>
-
- <Table
- className="sw-mt-4"
- columnCount={3}
- columnWidths={['50%', '25%', '25%']}
- header={
- <StyledTableRowHeader>
- <ContentCell className="sw-font-semibold sw-pl-4">
- {translate('quality_profile.rules.software_qualities_title')}
- </ContentCell>
- <NumericalCell className="sw-font-regular">{translate('active')}</NumericalCell>
- <NumericalCell className="sw-pr-4 sw-font-regular">
- {translate('inactive')}
- </NumericalCell>
- </StyledTableRowHeader>
- }
- noHeaderTopBorder
- noSidePadding
- withRoundedBorder
- >
- {Object.values(SoftwareQuality).map((quality) => (
- <ProfileRulesRow
- title={translate('software_quality', quality)}
- total={totalBySoftwareQuality[quality]?.count}
- count={countsBySoftwareImpact[quality]?.count}
- key={quality}
- qprofile={profile.key}
- propertyName={RulesFacetName.ImpactSoftwareQualities}
- propertyValue={quality}
- />
- ))}
- </Table>
-
- <div className="sw-mt-6 sw-flex sw-flex-col sw-gap-4 sw-items-start">
- {profile.activeDeprecatedRuleCount > 0 && (
- <ProfileRulesDeprecatedWarning
- activeDeprecatedRules={profile.activeDeprecatedRuleCount}
- profile={profile.key}
- />
- )}
+ <Spinner isLoading={isActivatedRulesLoading || isAllRulesLoading || isShowProfileLoading}>
+ <Heading className="sw-mb-4" as="h2">
+ {translate('quality_profile.rules.breakdown')}
+ </Heading>
- {isDefined(sonarWayDiff) && sonarWayDiff.missingRuleCount > 0 && (
- <ProfileRulesSonarWayComparison
- language={profile.language}
- profile={profile.key}
- sonarWayMissingRules={sonarWayDiff.missingRuleCount}
- sonarway={sonarWayDiff.profile}
- />
+ {isLegacy && (
+ <Table
+ columnCount={3}
+ columnWidths={['50%', '25%', '25%']}
+ header={
+ <StyledTableRowHeader>
+ <ContentCell className="sw-font-semibold sw-pl-4">{translate('type')}</ContentCell>
+ <NumericalCell className="sw-font-regular">{translate('active')}</NumericalCell>
+ <NumericalCell className="sw-pr-4 sw-font-regular">
+ {translate('inactive')}
+ </NumericalCell>
+ </StyledTableRowHeader>
+ }
+ noHeaderTopBorder
+ noSidePadding
+ withRoundedBorder
+ >
+ {RuleTypes.filter((type) => type !== 'UNKNOWN').map((type) => (
+ <ProfileRulesRow
+ title={translate('issue.type', type, 'plural')}
+ total={totalByTypes[type]?.count}
+ count={countsByTypes[type]?.count}
+ key={type}
+ qprofile={profile.key}
+ type={type}
+ />
+ ))}
+ </Table>
)}
- {actions.edit && !profile.isBuiltIn && (
- <ButtonPrimary className="it__quality-profiles__activate-rules" to={activateMoreUrl}>
- {translate('quality_profiles.activate_more')}
- </ButtonPrimary>
+ {!isLegacy && (
+ <>
+ <Table
+ className="sw-mb-4"
+ columnCount={3}
+ columnWidths={['50%', '25%', '25%']}
+ header={
+ <StyledTableRowHeader>
+ <ContentCell className="sw-font-semibold sw-pl-4">
+ {translate('quality_profile.rules.software_qualities_title')}
+ </ContentCell>
+ <NumericalCell className="sw-font-regular">{translate('active')}</NumericalCell>
+ <NumericalCell className="sw-pr-4 sw-font-regular">
+ {translate('inactive')}
+ </NumericalCell>
+ </StyledTableRowHeader>
+ }
+ noHeaderTopBorder
+ noSidePadding
+ withRoundedBorder
+ >
+ {Object.values(SoftwareQuality).map((quality) => (
+ <ProfileRulesRow
+ title={translate('software_quality', quality)}
+ total={totalBySoftwareQuality[quality]?.count}
+ count={countsBySoftwareImpact[quality]?.count}
+ key={quality}
+ qprofile={profile.key}
+ propertyName={RulesFacetName.ImpactSoftwareQualities}
+ propertyValue={quality}
+ />
+ ))}
+ </Table>
+
+ <Table
+ columnCount={3}
+ columnWidths={['50%', '25%', '25%']}
+ header={
+ <StyledTableRowHeader>
+ <ContentCell className="sw-font-semibold sw-pl-4">
+ {translate('quality_profile.rules.cct_categories_title')}
+ </ContentCell>
+ <NumericalCell className="sw-font-regular">{translate('active')}</NumericalCell>
+ <NumericalCell className="sw-pr-4 sw-font-regular">
+ {translate('inactive')}
+ </NumericalCell>
+ </StyledTableRowHeader>
+ }
+ noHeaderTopBorder
+ noSidePadding
+ withRoundedBorder
+ >
+ {Object.values(CleanCodeAttributeCategory).map((category) => (
+ <ProfileRulesRow
+ title={translate('rule.clean_code_attribute_category', category)}
+ total={totalByCctCategory[category]?.count}
+ count={countsByCctCategory[category]?.count}
+ key={category}
+ qprofile={profile.key}
+ propertyName={RulesFacetName.CleanCodeAttributeCategories}
+ propertyValue={category}
+ />
+ ))}
+ </Table>
+ </>
)}
- {/* if a user is allowed to `copy` a profile if they are a global admin */}
- {/* this user could potentially activate more rules if the profile was not built-in */}
- {/* in such cases it's better to show the button but disable it with a tooltip */}
- {actions.copy && profile.isBuiltIn && (
- <DocHelpTooltip content={translate('quality_profiles.activate_more.help.built_in')}>
- <ButtonPrimary className="it__quality-profiles__activate-rules" disabled>
+ <div className="sw-mt-6 sw-flex sw-flex-col sw-gap-4 sw-items-start">
+ {profile.activeDeprecatedRuleCount > 0 && (
+ <ProfileRulesDeprecatedWarning
+ activeDeprecatedRules={profile.activeDeprecatedRuleCount}
+ profile={profile.key}
+ />
+ )}
+
+ {isDefined(sonarWayDiff) && sonarWayDiff.missingRuleCount > 0 && (
+ <ProfileRulesSonarWayComparison
+ language={profile.language}
+ profile={profile.key}
+ sonarWayMissingRules={sonarWayDiff.missingRuleCount}
+ sonarway={sonarWayDiff.profile}
+ />
+ )}
+
+ {actions.edit && !profile.isBuiltIn && (
+ <ButtonPrimary className="it__quality-profiles__activate-rules" to={activateMoreUrl}>
{translate('quality_profiles.activate_more')}
</ButtonPrimary>
- </DocHelpTooltip>
- )}
- </div>
+ )}
+
+ {/* if a user is allowed to `copy` a profile if they are a global admin */}
+ {/* this user could potentially activate more rules if the profile was not built-in */}
+ {/* in such cases it's better to show the button but disable it with a tooltip */}
+ {actions.copy && profile.isBuiltIn && (
+ <DocHelpTooltip content={translate('quality_profiles.activate_more.help.built_in')}>
+ <ButtonPrimary className="it__quality-profiles__activate-rules" disabled>
+ {translate('quality_profiles.activate_more')}
+ </ButtonPrimary>
+ </DocHelpTooltip>
+ )}
+ </div>
+ </Spinner>
</section>
);
}