Переглянути джерело

SONAR-20547 Show new taxonomy in profile compare page

tags/10.3.0.82913
stanislavh 8 місяці тому
джерело
коміт
047d3846d5

+ 12
- 0
server/sonar-web/design-system/src/components/Text.tsx Переглянути файл

@@ -75,6 +75,14 @@ export function TextError({ text, className }: { className?: string; text: strin
);
}

export function TextSuccess({ text, className }: Readonly<{ className?: string; text: string }>) {
return (
<StyledTextSuccess className={className} title={text}>
{text}
</StyledTextSuccess>
);
}

export const StyledText = styled.span`
${tw`sw-inline-block`};
${tw`sw-truncate`};
@@ -104,6 +112,10 @@ const StyledTextError = styled(StyledText)`
color: ${themeColor('danger')};
`;

const StyledTextSuccess = styled(StyledText)`
color: ${themeColor('textSuccess')};
`;

export const DisabledText = styled.span`
${tw`sw-font-regular`};
color: ${themeColor('pageContentLight')};

+ 3
- 0
server/sonar-web/design-system/src/theme/light.ts Переглянути файл

@@ -76,6 +76,9 @@ export const lightTheme = {
// danger
danger: danger.dark,

// text
textSuccess: COLORS.yellowGreen[700],

//Project list card
projectCardDisabled: COLORS.blueGrey[200],


+ 10
- 3
server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts Переглянути файл

@@ -53,6 +53,7 @@ import {
compareProfiles,
copyProfile,
createQualityProfile,
deactivateRule,
deleteProfile,
dissociateProject,
getExporters,
@@ -111,6 +112,7 @@ export default class QualityProfilesServiceMock {
jest.mocked(searchRules).mockImplementation(this.handleSearchRules);
jest.mocked(compareProfiles).mockImplementation(this.handleCompareQualityProfiles);
jest.mocked(activateRule).mockImplementation(this.handleActivateRule);
jest.mocked(deactivateRule).mockImplementation(this.handleDeactivateRule);
jest.mocked(getRuleDetails).mockImplementation(this.handleGetRuleDetails);
jest.mocked(restoreQualityProfile).mockImplementation(this.handleRestoreQualityProfile);
jest.mocked(searchUsers).mockImplementation(this.handleSearchUsers);
@@ -556,10 +558,15 @@ export default class QualityProfilesServiceMock {
rule: string;
severity?: string;
}): Promise<undefined> => {
const profile = this.listQualityProfile.find((profile) => profile.key === data.key) as Profile;
const keyFilter = profile.name === this.comparisonResult.left.name ? 'inRight' : 'inLeft';
this.comparisonResult.inRight = this.comparisonResult.inRight.filter(
({ key }) => key !== data.rule,
);

return this.reply(undefined);
};

this.comparisonResult[keyFilter] = this.comparisonResult[keyFilter].filter(
handleDeactivateRule = (data: { key: string; rule: string }) => {
this.comparisonResult.inLeft = this.comparisonResult.inLeft.filter(
({ key }) => key !== data.rule,
);


+ 25
- 8
server/sonar-web/src/main/js/api/quality-profiles.ts Переглянути файл

@@ -22,6 +22,11 @@ import { Exporter, ProfileChangelogEvent } from '../apps/quality-profiles/types'
import { csvEscape } from '../helpers/csv';
import { throwGlobalError } from '../helpers/error';
import { RequestData, getJSON, post, postJSON } from '../helpers/request';
import {
CleanCodeAttributeCategory,
SoftwareImpactSeverity,
SoftwareQuality,
} from '../types/clean-code-taxonomy';
import { Dict, Paging, ProfileInheritanceDetails, UserSelected } from '../types/types';

export interface ProfileActions {
@@ -187,17 +192,29 @@ export function getProfileChangelog(
});
}

export interface RuleCompare {
key: string;
name: string;
cleanCodeAttributeCategory: CleanCodeAttributeCategory;
impacts: Array<{
softwareQuality: SoftwareQuality;
severity: SoftwareImpactSeverity;
}>;
left?: { params: Dict<string>; severity: string };
right?: { params: Dict<string>; severity: string };
}

export interface CompareResponse {
left: { name: string };
right: { name: string };
inLeft: Array<{ key: string; name: string; severity: string }>;
inRight: Array<{ key: string; name: string; severity: string }>;
modified: Array<{
key: string;
name: string;
left: { params: Dict<string>; severity: string };
right: { params: Dict<string>; severity: string };
}>;
inLeft: Array<RuleCompare>;
inRight: Array<RuleCompare>;
modified: Array<
RuleCompare & {
left: { params: Dict<string>; severity: string };
right: { params: Dict<string>; severity: string };
}
>;
}

export function compareProfiles(leftKey: string, rightKey: string): Promise<CompareResponse> {

+ 40
- 4
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx Переглянути файл

@@ -21,6 +21,7 @@ import { act, getByText, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import selectEvent from 'react-select-event';
import QualityProfilesServiceMock from '../../../api/mocks/QualityProfilesServiceMock';
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import { mockPaging, mockRule } from '../../../helpers/testMocks';
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { byRole, byText } from '../../../helpers/testSelector';
@@ -30,9 +31,11 @@ jest.mock('../../../api/quality-profiles');
jest.mock('../../../api/rules');

const serviceMock = new QualityProfilesServiceMock();
const settingsMock = new SettingsServiceMock();

beforeEach(() => {
serviceMock.reset();
settingsMock.reset();
});

const ui = {
@@ -83,6 +86,11 @@ const ui = {
byRole('button', {
name: `quality_profiles.comparison.activate_rule.${profileName}`,
}),
deactivateRuleButton: (profileName: string) =>
byRole('button', {
name: `quality_profiles.comparison.deactivate_rule.${profileName}`,
}),
deactivateConfirmButton: byRole('button', { name: 'yes' }),
activateConfirmButton: byRole('button', { name: 'coding_rules.activate' }),
namePropupInput: byRole('textbox', { name: 'quality_profiles.new_name required' }),
filterByLang: byRole('combobox', { name: 'quality_profiles.select_lang' }),
@@ -103,6 +111,8 @@ const ui = {
nameCreatePopupInput: byRole('textbox', { name: 'name required' }),
importerA: byText('Importer A'),
importerB: byText('Importer B'),
summaryAdditionalRules: (count: number) => byText(`quality_profile.summary_additional.${count}`),
summaryFewerRules: (count: number) => byText(`quality_profile.summary_fewer.${count}`),
comparisonDiffTableHeading: (rulesQuantity: number, profileName: string) =>
byRole('columnheader', {
name: `quality_profiles.x_rules_only_in.${rulesQuantity}.${profileName}`,
@@ -321,18 +331,44 @@ it('should be able to compare profiles', async () => {
expect(ui.changelogLink.query()).not.toBeInTheDocument();

await selectEvent.select(ui.compareDropdown.get(), 'java quality profile #2');
expect(ui.comparisonDiffTableHeading(1, 'java quality profile').get()).toBeInTheDocument();
expect(await ui.comparisonDiffTableHeading(1, 'java quality profile').find()).toBeInTheDocument();
expect(ui.comparisonDiffTableHeading(1, 'java quality profile #2').get()).toBeInTheDocument();
expect(ui.comparisonModifiedTableHeading(1).get()).toBeInTheDocument();

// java quality profile is not editable
expect(ui.activeRuleButton('java quality profile').query()).not.toBeInTheDocument();
expect(ui.deactivateRuleButton('java quality profile').query()).not.toBeInTheDocument();
});

await user.click(ui.activeRuleButton('java quality profile #2').get());
it('should be able to activate or deactivate rules in comparison page', async () => {
// From the list page
const user = userEvent.setup();
serviceMock.setAdmin();
renderQualityProfiles();

await user.click(await ui.listProfileActions('java quality profile #2', 'Java').find());
await user.click(ui.compareButton.get());
await selectEvent.select(ui.compareDropdown.get(), 'java quality profile');

expect(await ui.summaryFewerRules(1).find()).toBeInTheDocument();
expect(ui.summaryAdditionalRules(1).get()).toBeInTheDocument();

// Activate
await act(async () => {
await user.click(ui.activeRuleButton('java quality profile #2').get());
});
expect(ui.popup.get()).toBeInTheDocument();

await user.click(ui.activateConfirmButton.get());
expect(ui.comparisonDiffTableHeading(1, 'java quality profile').query()).not.toBeInTheDocument();
await act(async () => {
await user.click(ui.activateConfirmButton.get());
});
expect(ui.summaryFewerRules(1).query()).not.toBeInTheDocument();

// Deactivate
await user.click(await ui.deactivateRuleButton('java quality profile #2').find());
expect(ui.popup.get()).toBeInTheDocument();
await user.click(ui.deactivateConfirmButton.get());
expect(ui.summaryAdditionalRules(1).query()).not.toBeInTheDocument();
});

function renderQualityProfiles() {

+ 50
- 86
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx Переглянути файл

@@ -19,8 +19,10 @@
*/
import { Spinner } from 'design-system';
import * as React from 'react';
import { compareProfiles, CompareResponse } from '../../../api/quality-profiles';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import { useLocation, useRouter } from '../../../components/hoc/withRouter';
import { useProfilesCompareQuery } from '../../../queries/quality-profiles';
import { useGetValueQuery } from '../../../queries/settings';
import { SettingsKey } from '../../../types/settings';
import { withQualityProfilesContext } from '../qualityProfilesContext';
import { Profile } from '../types';
import { getProfileComparePath } from '../utils';
@@ -30,99 +32,61 @@ import ComparisonResults from './ComparisonResults';
interface Props {
profile: Profile;
profiles: Profile[];
location: Location;
router: Router;
}

type State = { loading: boolean } & Partial<CompareResponse>;
type StateWithResults = { loading: boolean } & CompareResponse;
export function ComparisonContainer(props: Readonly<Props>) {
const { profile, profiles } = props;
const location = useLocation();
const router = useRouter();
const { data: inheritRulesSetting } = useGetValueQuery(
SettingsKey.QPAdminCanDisableInheritedRules,
);
const canDeactivateInheritedRules = inheritRulesSetting?.value === 'true';

class ComparisonContainer extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false };
const { withKey } = location.query;
const {
data: compareResults,
isLoading,
refetch,
} = useProfilesCompareQuery(profile.key, withKey);

componentDidMount() {
this.mounted = true;
this.loadResults();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.profile !== this.props.profile || prevProps.location !== this.props.location) {
this.loadResults();
}
}

componentWillUnmount() {
this.mounted = false;
}

loadResults = () => {
const { withKey } = this.props.location.query;
if (!withKey) {
this.setState({ left: undefined, loading: false });
return Promise.resolve();
}

this.setState({ loading: true });
return compareProfiles(this.props.profile.key, withKey).then(
({ left, right, inLeft, inRight, modified }) => {
if (this.mounted) {
this.setState({ left, right, inLeft, inRight, modified, loading: false });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
},
);
const handleCompare = (withKey: string) => {
const path = getProfileComparePath(profile.name, profile.language, withKey);
router.push(path);
};

handleCompare = (withKey: string) => {
const path = getProfileComparePath(
this.props.profile.name,
this.props.profile.language,
withKey,
);
this.props.router.push(path);
const refresh = async () => {
await refetch();
};

hasResults(state: State): state is StateWithResults {
return state.left !== undefined;
}
return (
<div className="sw-body-sm">
<div className="sw-flex sw-items-center">
<ComparisonForm
onCompare={handleCompare}
profile={profile}
profiles={profiles}
withKey={withKey}
/>

render() {
const { profile, profiles, location } = this.props;
const { withKey } = location.query;

return (
<div className="sw-body-sm">
<div className="sw-flex sw-items-center">
<ComparisonForm
onCompare={this.handleCompare}
profile={profile}
profiles={profiles}
withKey={withKey}
/>

<Spinner className="sw-ml-2" loading={this.state.loading} />
</div>

{this.hasResults(this.state) && (
<ComparisonResults
inLeft={this.state.inLeft}
inRight={this.state.inRight}
left={this.state.left}
leftProfile={profile}
modified={this.state.modified}
refresh={this.loadResults}
right={this.state.right}
rightProfile={profiles.find((p) => p.key === withKey)}
/>
)}
<Spinner className="sw-ml-2" loading={isLoading} />
</div>
);
}

{compareResults && (
<ComparisonResults
inLeft={compareResults.inLeft}
inRight={compareResults.inRight}
left={compareResults.left}
leftProfile={profile}
modified={compareResults.modified}
refresh={refresh}
right={compareResults.right}
rightProfile={profiles.find((p) => p.key === withKey)}
canDeactivateInheritedRules={canDeactivateInheritedRules}
/>
)}
</div>
);
}

export default withQualityProfilesContext(withRouter(ComparisonContainer));
export default withQualityProfilesContext(ComparisonContainer);

+ 79
- 0
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultDeactivation.tsx Переглянути файл

@@ -0,0 +1,79 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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 { DangerButtonSecondary } from 'design-system';
import { noop } from 'lodash';
import * as React from 'react';
import { useIntl } from 'react-intl';
import { Profile, deactivateRule } from '../../../api/quality-profiles';
import ConfirmButton from '../../../components/controls/ConfirmButton';
import Tooltip from '../../../components/controls/Tooltip';

interface Props {
onDone: () => Promise<void>;
profile: Profile;
ruleKey: string;
canDeactivateInheritedRules: boolean;
}

export default function ComparisonResultDeactivation(props: React.PropsWithChildren<Props>) {
const { profile, ruleKey, canDeactivateInheritedRules } = props;
const intl = useIntl();

const handleDeactivate = () => {
const data = {
key: profile.key,
rule: ruleKey,
};
deactivateRule(data).then(props.onDone, noop);
};

return (
<ConfirmButton
confirmButtonText={intl.formatMessage({ id: 'yes' })}
modalBody={intl.formatMessage({ id: 'coding_rules.deactivate.confirm' })}
modalHeader={intl.formatMessage({ id: 'coding_rules.deactivate' })}
onConfirm={handleDeactivate}
>
{({ onClick }) => (
<Tooltip
overlay={
canDeactivateInheritedRules
? intl.formatMessage(
{ id: 'quality_profiles.comparison.deactivate_rule' },
{ profile: profile.name },
)
: intl.formatMessage({ id: 'coding_rules.can_not_deactivate' })
}
>
<DangerButtonSecondary
disabled={!canDeactivateInheritedRules}
onClick={onClick}
aria-label={intl.formatMessage(
{ id: 'quality_profiles.comparison.deactivate_rule' },
{ profile: profile.name },
)}
>
{intl.formatMessage({ id: 'coding_rules.deactivate' })}
</DangerButtonSecondary>
</Tooltip>
)}
</ConfirmButton>
);
}

+ 88
- 47
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.tsx Переглянути файл

@@ -18,12 +18,19 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { ActionCell, ContentCell, Link, Table, TableRowInteractive } from 'design-system';
import { isEqual } from 'lodash';
import * as React from 'react';
import { useIntl } from 'react-intl';
import { CompareResponse, Profile } from '../../../api/quality-profiles';
import { CompareResponse, Profile, RuleCompare } from '../../../api/quality-profiles';
import IssueSeverityIcon from '../../../components/icon-mappers/IssueSeverityIcon';
import { CleanCodeAttributePill } from '../../../components/shared/CleanCodeAttributePill';
import SoftwareImpactPill from '../../../components/shared/SoftwareImpactPill';
import { getRulesUrl } from '../../../helpers/urls';
import { IssueSeverity } from '../../../types/issues';
import { Dict } from '../../../types/types';
import ComparisonResultActivation from './ComparisonResultActivation';
import ComparisonResultDeactivation from './ComparisonResultDeactivation';
import ComparisonResultsSummary from './ComparisonResultsSummary';

type Params = Dict<string>;

@@ -31,54 +38,33 @@ interface Props extends CompareResponse {
leftProfile: Profile;
refresh: () => Promise<void>;
rightProfile?: Profile;
canDeactivateInheritedRules: boolean;
}

export default function ComparisonResults(props: Readonly<Props>) {
const { leftProfile, rightProfile, inLeft, left, right, inRight, modified } = props;
const {
leftProfile,
rightProfile,
inLeft,
left,
right,
inRight,
modified,
canDeactivateInheritedRules,
} = props;

const intl = useIntl();

const emptyComparison = !inLeft.length && !inRight.length && !modified.length;

const canActivate = (profile: Profile) =>
!profile.isBuiltIn && profile.actions && profile.actions.edit;

const renderRule = React.useCallback((rule: { key: string; name: string }) => {
return (
<div>
<Link className="sw-ml-1" to={getRulesUrl({ rule_key: rule.key, open: rule.key })}>
{rule.name}
</Link>
</div>
);
}, []);

const renderParameters = React.useCallback((params: Params) => {
if (!params) {
return null;
}
return (
<ul>
{Object.keys(params).map((key) => (
<li className="sw-mt-2 sw-break-all" key={key}>
<code className="sw-code">
{key}
{': '}
{params[key]}
</code>
</li>
))}
</ul>
);
}, []);
const canEdit = (profile: Profile) => !profile.isBuiltIn && profile.actions?.edit;

const renderLeft = () => {
if (inLeft.length === 0) {
return null;
}

const renderSecondColumn = rightProfile && canActivate(rightProfile);

const canRenderSecondColumn = leftProfile && canEdit(leftProfile);
return (
<Table
columnCount={2}
@@ -94,7 +80,7 @@ export default function ComparisonResults(props: Readonly<Props>) {
{ count: inLeft.length, profile: left.name },
)}
</ContentCell>
{renderSecondColumn && (
{canRenderSecondColumn && (
<ContentCell aria-label={intl.formatMessage({ id: 'actions' })}>&nbsp;</ContentCell>
)}
</TableRowInteractive>
@@ -102,14 +88,17 @@ export default function ComparisonResults(props: Readonly<Props>) {
>
{inLeft.map((rule) => (
<TableRowInteractive key={`left-${rule.key}`}>
<ContentCell>{renderRule(rule)}</ContentCell>
{renderSecondColumn && (
<ContentCell>
<RuleCell rule={rule} />
</ContentCell>
{canRenderSecondColumn && (
<ContentCell className="sw-px-0">
<ComparisonResultActivation
<ComparisonResultDeactivation
key={rule.key}
onDone={props.refresh}
profile={rightProfile}
profile={leftProfile}
ruleKey={rule.key}
canDeactivateInheritedRules={canDeactivateInheritedRules}
/>
</ContentCell>
)}
@@ -124,7 +113,7 @@ export default function ComparisonResults(props: Readonly<Props>) {
return null;
}

const renderFirstColumn = leftProfile && canActivate(leftProfile);
const renderFirstColumn = leftProfile && canEdit(leftProfile);

return (
<Table
@@ -159,7 +148,9 @@ export default function ComparisonResults(props: Readonly<Props>) {
/>
</ActionCell>
)}
<ContentCell className="sw-pl-4">{renderRule(rule)}</ContentCell>
<ContentCell className="sw-pl-4">
<RuleCell rule={rule} />
</ContentCell>
</TableRowInteractive>
))}
</Table>
@@ -195,14 +186,14 @@ export default function ComparisonResults(props: Readonly<Props>) {
<TableRowInteractive key={`modified-${rule.key}`}>
<ContentCell>
<div>
{renderRule(rule)}
{renderParameters(rule.left.params)}
<RuleCell rule={rule} severity={rule.left.severity} />
<Parameters params={rule.left.params} />
</div>
</ContentCell>
<ContentCell className="sw-pl-4">
<div>
{renderRule(rule)}
{renderParameters(rule.right.params)}
<RuleCell rule={rule} severity={rule.right.severity} />
<Parameters params={rule.right.params} />
</div>
</ContentCell>
</TableRowInteractive>
@@ -212,11 +203,17 @@ export default function ComparisonResults(props: Readonly<Props>) {
};

return (
<div className="sw-mt-4">
<div className="sw-mt-8">
{emptyComparison ? (
intl.formatMessage({ id: 'quality_profile.empty_comparison' })
) : (
<>
<ComparisonResultsSummary
profileName={leftProfile.name}
comparedProfileName={rightProfile?.name}
additionalCount={inLeft.length}
fewerCount={inRight.length}
/>
{renderLeft()}
{renderRight()}
{renderModified()}
@@ -225,3 +222,47 @@ export default function ComparisonResults(props: Readonly<Props>) {
</div>
);
}

function RuleCell({ rule, severity }: Readonly<{ rule: RuleCompare; severity?: string }>) {
const shouldRenderSeverity =
Boolean(severity) && rule.left && rule.right && isEqual(rule.left.params, rule.right.params);

return (
<div>
{shouldRenderSeverity && <IssueSeverityIcon severity={severity as IssueSeverity} />}
<Link className="sw-ml-1" to={getRulesUrl({ rule_key: rule.key, open: rule.key })}>
{rule.name}
</Link>
<ul className="sw-mt-3 sw-flex sw-items-center">
<li>
<CleanCodeAttributePill cleanCodeAttributeCategory={rule.cleanCodeAttributeCategory} />
</li>
{rule.impacts.map(({ severity, softwareQuality }) => (
<li key={softwareQuality} className="sw-ml-2">
<SoftwareImpactPill type="rule" quality={softwareQuality} severity={severity} />
</li>
))}
</ul>
</div>
);
}

function Parameters({ params }: Readonly<{ params?: Params }>) {
if (!params) {
return null;
}

return (
<ul>
{Object.keys(params).map((key) => (
<li className="sw-mt-2 sw-break-all" key={key}>
<code className="sw-body-sm">
{key}
{': '}
{params[key]}
</code>
</li>
))}
</ul>
);
}

+ 101
- 0
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultsSummary.tsx Переглянути файл

@@ -0,0 +1,101 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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 { TextError, TextSuccess } from 'design-system';
import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';

interface Props {
profileName: string;
comparedProfileName?: string;
additionalCount: number;
fewerCount: number;
}

export default function ComparisonResultsSummary(props: Readonly<Props>) {
const { profileName, comparedProfileName, additionalCount, fewerCount } = props;
const intl = useIntl();

if (additionalCount === 0 && fewerCount === 0) {
return null;
}

if (additionalCount === 0 || fewerCount === 0) {
return (
<div className="sw-mb-4">
<FormattedMessage
id="quality_profile.summary_differences2"
values={{
profile: profileName,
comparedProfile: comparedProfileName,
difference:
additionalCount === 0 ? (
<TextError
className="sw-inline"
text={intl.formatMessage(
{ id: 'quality_profile.summary_fewer' },
{ count: fewerCount },
)}
/>
) : (
<TextSuccess
className="sw-inline"
text={intl.formatMessage(
{ id: 'quality_profile.summary_additional' },
{ count: additionalCount },
)}
/>
),
}}
/>
</div>
);
}

return (
<div className="sw-mb-4">
<FormattedMessage
id="quality_profile.summary_differences1"
values={{
profile: profileName,
comparedProfile: comparedProfileName,
additional: (
<TextSuccess
className="sw-inline"
text={intl.formatMessage(
{ id: 'quality_profile.summary_additional' },
{ count: additionalCount },
)}
/>
),
fewer: (
<TextError
className="sw-inline"
text={intl.formatMessage(
{ id: 'quality_profile.summary_fewer' },
{ count: fewerCount },
)}
/>
),
}}
/>
</div>
);
}

+ 21
- 2
server/sonar-web/src/main/js/helpers/testMocks.ts Переглянути файл

@@ -481,18 +481,37 @@ export function mockCompareResult(overrides: Partial<CompareResponse> = {}): Com
{
key: 'java:S4604',
name: 'Rule in left',
severity: 'MINOR',
cleanCodeAttributeCategory: CleanCodeAttributeCategory.Adaptable,
impacts: [
{
softwareQuality: SoftwareQuality.Maintainability,
severity: SoftwareImpactSeverity.Medium,
},
],
},
],
inRight: [
{
key: 'java:S5128',
name: 'Rule in right',
severity: 'MAJOR',
cleanCodeAttributeCategory: CleanCodeAttributeCategory.Responsible,
impacts: [
{
softwareQuality: SoftwareQuality.Security,
severity: SoftwareImpactSeverity.Medium,
},
],
},
],
modified: [
{
cleanCodeAttributeCategory: CleanCodeAttributeCategory.Consistent,
impacts: [
{
softwareQuality: SoftwareQuality.Maintainability,
severity: SoftwareImpactSeverity.Low,
},
],
key: 'java:S1698',
name: '== and != should not be used when equals is overridden',
left: { params: {}, severity: 'MINOR' },

+ 14
- 0
server/sonar-web/src/main/js/queries/quality-profiles.ts Переглянути файл

@@ -24,6 +24,7 @@ import {
Profile,
addGroup,
addUser,
compareProfiles,
getProfileInheritance,
} from '../api/quality-profiles';
import { ProfileInheritanceDetails } from '../types/types';
@@ -49,6 +50,19 @@ export function useProfileInheritanceQuery(
});
}

export function useProfilesCompareQuery(leftKey: string, rightKey: string) {
return useQuery({
queryKey: ['quality-profiles', 'compare', leftKey, rightKey],
queryFn: ({ queryKey: [, , leftKey, rightKey] }) => {
if (!leftKey || !rightKey) {
return null;
}

return compareProfiles(leftKey, rightKey);
},
});
}

export function useAddUserMutation(onSuccess: () => unknown) {
return useMutation({
mutationFn: (data: AddRemoveUserParameters) => addUser(data),

+ 6
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -2005,7 +2005,11 @@ quality_profiles.warning.is_default_no_rules=The current profile is the default
quality_profiles.x_sonarway_missing_rules={linkCount} Sonar way {count, plural, one {rule} other {rules}} not included
quality_profiles.parent=Parent:
quality_profiles.parameter_set_to=Parameter {0} set to {1}
quality_profiles.x_rules_only_in={count} rules only in {profile}
quality_profile.summary_additional={count} additional {count, plural, one {rule} other {rules}}
quality_profile.summary_fewer={count} fewer {count, plural, one {rule} other {rules}}
quality_profile.summary_differences1={profile} has {additional} and {fewer} than {comparedProfile}.
quality_profile.summary_differences2={profile} has {difference} than {comparedProfile}
quality_profiles.x_rules_only_in={count} rules in {profile}
quality_profiles.x_rules_have_different_configuration={count} rules have a different configuration
quality_profiles.copy_x_title=Copy Profile "{0}" - {1}
quality_profiles.extend_x_title=Extend Profile "{0}" - {1}
@@ -2044,6 +2048,7 @@ quality_profiles.activate_more=Activate More
quality_profiles.activate_more.help.built_in=This quality profile is built in, and cannot be updated manually. If you want to activate more rules, create a new profile that inherits from this one and add rules there.
quality_profiles.activate_more_rules=Activate More Rules
quality_profiles.comparison.activate_rule=Activate rule for profile "{profile}"
quality_profiles.comparison.deactivate_rule=Dectivate rule for profile "{profile}"
quality_profiles.intro=Quality profiles are collections of rules to apply during an analysis. For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default. Ideally, all projects will use the same profile for a language.
quality_profiles.list.projects=Projects
quality_profiles.list.projects.help=Projects assigned to a profile will always be analyzed with it for that language, regardless of which profile is the default. Quality profile administrators may assign projects to a non-default profile, or always make it follow the system default. Project administrators may choose any profile for each language.

Завантаження…
Відмінити
Зберегти