Browse Source

SONAR-20548 Migrate quality profile changelog page to CCT

tags/10.3.0.82913
7PH 8 months ago
parent
commit
a898251506

+ 32
- 2
server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts View File

@@ -29,8 +29,14 @@ import {
mockRuleDetails,
mockUserSelected,
} from '../../helpers/testMocks';
import { CleanCodeAttributeCategory, SoftwareQuality } from '../../types/clean-code-taxonomy';
import {
CleanCodeAttribute,
CleanCodeAttributeCategory,
SoftwareImpactSeverity,
SoftwareQuality,
} from '../../types/clean-code-taxonomy';
import { SearchRulesResponse } from '../../types/coding-rules';
import { IssueSeverity } from '../../types/issues';
import { SearchRulesQuery } from '../../types/rules';
import { Dict, Paging, ProfileInheritanceDetails, RuleDetails } from '../../types/types';
import {
@@ -226,6 +232,13 @@ export default class QualityProfilesServiceMock {
action: 'DEACTIVATED',
ruleKey: 'php:rule1',
ruleName: 'PHP Rule',
params: {
severity: IssueSeverity.Critical,
newCleanCodeAttribute: CleanCodeAttribute.Complete,
newCleanCodeAttributeCategory: CleanCodeAttributeCategory.Intentional,
oldCleanCodeAttribute: CleanCodeAttribute.Clear,
oldCleanCodeAttributeCategory: CleanCodeAttributeCategory.Responsible,
},
}),
mockQualityProfileChangelogEvent({
date: '2019-05-23T03:12:32+0100',
@@ -245,6 +258,23 @@ export default class QualityProfilesServiceMock {
action: 'DEACTIVATED',
ruleKey: 'c:rule1',
ruleName: 'Rule 1',
params: {
severity: IssueSeverity.Critical,
newCleanCodeAttribute: CleanCodeAttribute.Complete,
newCleanCodeAttributeCategory: CleanCodeAttributeCategory.Intentional,
oldCleanCodeAttribute: CleanCodeAttribute.Lawful,
oldCleanCodeAttributeCategory: CleanCodeAttributeCategory.Responsible,
impactChanges: [
{
newSeverity: SoftwareImpactSeverity.Medium,
newSoftwareQuality: SoftwareQuality.Reliability,
},
{
oldSeverity: SoftwareImpactSeverity.High,
oldSoftwareQuality: SoftwareQuality.Maintainability,
},
],
},
}),
mockQualityProfileChangelogEvent({
date: '2019-04-23T02:12:32+0100',
@@ -259,7 +289,7 @@ export default class QualityProfilesServiceMock {
ruleName: 'Rule 2',
authorName: 'John Doe',
params: {
severity: 'CRITICAL',
severity: IssueSeverity.Critical,
credentialWords: 'foo,bar',
},
}),

+ 40
- 14
server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx View File

@@ -18,11 +18,21 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { isSameMinute } from 'date-fns';
import { ContentCell, Link, Note, Table, TableRow, TableRowInteractive } from 'design-system';
import {
CellComponent,
ContentCell,
Link,
Note,
Table,
TableRow,
TableRowInteractive,
} from 'design-system';
import { sortBy } from 'lodash';
import * as React from 'react';
import { useIntl } from 'react-intl';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { CleanCodeAttributePill } from '../../../components/shared/CleanCodeAttributePill';
import SoftwareImpactPill from '../../../components/shared/SoftwareImpactPill';
import { parseDate } from '../../../helpers/dates';
import { getRulesUrl } from '../../../helpers/urls';
import { ProfileChangelogEvent } from '../types';
@@ -35,7 +45,6 @@ interface Props {
export default function Changelog(props: Props) {
const intl = useIntl();

let isEvenRow = false;
const sortedRows = sortBy(
props.events,
// sort events by date, rounded to a minute, recent events first
@@ -52,30 +61,46 @@ export default function Changelog(props: Props) {
prev.authorName === event.authorName &&
prev.action === event.action;

if (!isBulkChange) {
isEvenRow = !isEvenRow;
}

return (
<TableRowInteractive key={`${event.date}-${event.ruleKey}`}>
<ContentCell className="sw-whitespace-nowrap">
<TableRowInteractive key={index}>
<ContentCell className="sw-whitespace-nowrap sw-align-top">
{!isBulkChange && <DateTimeFormatter date={event.date} />}
</ContentCell>

<ContentCell className="sw-whitespace-nowrap sw-max-w-[120px]">
<ContentCell className="sw-whitespace-nowrap sw-align-top sw-max-w-[120px]">
{!isBulkChange && (event.authorName ? event.authorName : <Note>System</Note>)}
</ContentCell>

<ContentCell className="sw-whitespace-nowrap">
<ContentCell className="sw-whitespace-nowrap sw-align-top">
{!isBulkChange &&
intl.formatMessage({ id: `quality_profiles.changelog.${event.action}` })}
</ContentCell>

<ContentCell>
<Link to={getRulesUrl({ rule_key: event.ruleKey })}>{event.ruleName}</Link>
</ContentCell>
<CellComponent className="sw-align-top">
{event.ruleName && (
<Link to={getRulesUrl({ rule_key: event.ruleKey })} className="sw-block sw-w-fit">
{event.ruleName}
</Link>
)}
<div className="sw-mt-2 sw-flex sw-gap-2">
{event.cleanCodeAttributeCategory && (
<CleanCodeAttributePill
cleanCodeAttributeCategory={event.cleanCodeAttributeCategory}
/>
)}
{event.impacts?.map((impact) => (
<SoftwareImpactPill
key={impact.softwareQuality}
quality={impact.softwareQuality}
severity={impact.severity}
/>
))}
</div>
</CellComponent>

<ContentCell>{event.params && <ChangesList changes={event.params} />}</ContentCell>
<ContentCell className="sw-align-top sw-max-w-[400px]">
{event.params && <ChangesList changes={event.params} />}
</ContentCell>
</TableRowInteractive>
);
});
@@ -83,6 +108,7 @@ export default function Changelog(props: Props) {
return (
<Table
columnCount={5}
columnWidths={['1%', '1%', '1%', 'auto', '1%']}
header={
<TableRow>
<ContentCell>{intl.formatMessage({ id: 'date' })}</ContentCell>

+ 42
- 13
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.tsx View File

@@ -18,29 +18,58 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Dict } from '../../../types/types';
import { ProfileChangelogEvent } from '../types';
import CleanCodeAttributeChange from './CleanCodeAttributeChange';
import ParameterChange from './ParameterChange';
import SeverityChange from './SeverityChange';
import SoftwareImpactChange from './SoftwareImpactChange';

interface Props {
changes: Dict<string | null>;
changes: ProfileChangelogEvent['params'];
}

export default function ChangesList({ changes }: Props) {
const renderSeverity = (key: string) => {
const severity = changes[key];
return severity ? <SeverityChange severity={severity} /> : null;
};
const {
severity,
oldCleanCodeAttribute,
oldCleanCodeAttributeCategory,
newCleanCodeAttribute,
newCleanCodeAttributeCategory,
impactChanges,
...rest
} = changes ?? {};

return (
<ul className="sw-flex sw-flex-col sw-gap-1">
{Object.keys(changes).map((key) => (
<ul className="sw-w-full sw-flex sw-flex-col sw-gap-1">
{severity && (
<li>
<SeverityChange severity={severity} />
</li>
)}

{oldCleanCodeAttribute &&
oldCleanCodeAttributeCategory &&
newCleanCodeAttribute &&
newCleanCodeAttributeCategory && (
<li>
<CleanCodeAttributeChange
oldCleanCodeAttribute={oldCleanCodeAttribute}
oldCleanCodeAttributeCategory={oldCleanCodeAttributeCategory}
newCleanCodeAttribute={newCleanCodeAttribute}
newCleanCodeAttributeCategory={newCleanCodeAttributeCategory}
/>
</li>
)}

{impactChanges?.map((impactChange, index) => (
<li key={index}>
<SoftwareImpactChange impactChange={impactChange} />
</li>
))}

{Object.keys(rest).map((key) => (
<li key={key}>
{key === 'severity' ? (
renderSeverity(key)
) : (
<ParameterChange name={key} value={changes[key]} />
)}
<ParameterChange name={key} value={rest[key] as string | null} />
</li>
))}
</ul>

+ 65
- 0
server/sonar-web/src/main/js/apps/quality-profiles/changelog/CleanCodeAttributeChange.tsx View File

@@ -0,0 +1,65 @@
/*
* 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 * as React from 'react';
import { useIntl } from 'react-intl';
import { CleanCodeAttribute, CleanCodeAttributeCategory } from '../../../types/clean-code-taxonomy';

interface Props {
oldCleanCodeAttribute: CleanCodeAttribute;
oldCleanCodeAttributeCategory: CleanCodeAttributeCategory;
newCleanCodeAttribute: CleanCodeAttribute;
newCleanCodeAttributeCategory: CleanCodeAttributeCategory;
}

export default function CleanCodeAttributeChange(props: Readonly<Props>) {
const {
oldCleanCodeAttribute,
oldCleanCodeAttributeCategory,
newCleanCodeAttribute,
newCleanCodeAttributeCategory,
} = props;

const intl = useIntl();

const onlyAttributeChanged = oldCleanCodeAttributeCategory === newCleanCodeAttributeCategory;

const labels = {
newCleanCodeAttribute: intl.formatMessage({
id: `issue.clean_code_attribute.${newCleanCodeAttribute}`,
}),
newCleanCodeAttributeCategory: intl.formatMessage({
id: `issue.clean_code_attribute_category.${newCleanCodeAttributeCategory}`,
}),
oldCleanCodeAttribute: intl.formatMessage({
id: `issue.clean_code_attribute.${oldCleanCodeAttribute}`,
}),
oldCleanCodeAttributeCategory: intl.formatMessage({
id: `issue.clean_code_attribute_category.${oldCleanCodeAttributeCategory}`,
}),
};

return (
<div>
{onlyAttributeChanged
? intl.formatMessage({ id: 'quality_profiles.changelog.cca_only_changed' }, labels)
: intl.formatMessage({ id: 'quality_profiles.changelog.cca_and_category_changed' }, labels)}
</div>
);
}

+ 63
- 0
server/sonar-web/src/main/js/apps/quality-profiles/changelog/SoftwareImpactChange.tsx View File

@@ -0,0 +1,63 @@
/*
* 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 * as React from 'react';
import { useIntl } from 'react-intl';
import { ProfileChangelogEventImpactChange } from '../types';

interface Props {
impactChange: ProfileChangelogEventImpactChange;
}

export default function SoftwareImpactChange({ impactChange }: Readonly<Props>) {
const { oldSeverity, oldSoftwareQuality, newSeverity, newSoftwareQuality } = impactChange;

const intl = useIntl();

const labels = {
oldSeverity: intl.formatMessage({ id: `severity.${oldSeverity}` }),
oldSoftwareQuality: intl.formatMessage({ id: `issue.software_quality.${oldSoftwareQuality}` }),
newSeverity: intl.formatMessage({ id: `severity.${newSeverity}` }),
newSoftwareQuality: intl.formatMessage({ id: `issue.software_quality.${newSoftwareQuality}` }),
};

const isChanged = oldSeverity && oldSoftwareQuality && newSeverity && newSoftwareQuality;
const isAdded = !oldSeverity && !oldSoftwareQuality && newSeverity && newSoftwareQuality;
const isRemoved = oldSeverity && oldSoftwareQuality && !newSeverity && !newSoftwareQuality;

if (isChanged) {
return (
<div>{intl.formatMessage({ id: 'quality_profiles.changelog.impact_changed' }, labels)}</div>
);
}

if (isAdded) {
return (
<div>{intl.formatMessage({ id: 'quality_profiles.changelog.impact_added' }, labels)}</div>
);
}

if (isRemoved) {
return (
<div>{intl.formatMessage({ id: 'quality_profiles.changelog.impact_removed' }, labels)}</div>
);
}

return null;
}

+ 50
- 18
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogContainer-it.tsx View File

@@ -31,12 +31,37 @@ jest.mock('../../../../api/quality-profiles');
const serviceMock = new QualityProfilesServiceMock();
const ui = {
row: byRole('row'),
cell: byRole('cell'),
link: byRole('link'),
emptyPage: byText('no_results'),
showMore: byRole('button', { name: 'show_more' }),
startDate: byRole('textbox', { name: 'start_date' }),
endDate: byRole('textbox', { name: 'end_date' }),
reset: byRole('button', { name: 'reset_verb' }),

checkRow: (
index: number,
date: string,
user: string,
action: string,
rule: string | null,
updates: RegExp[] = [],
) => {
const row = ui.row.getAll()[index];
if (!row) {
throw new Error(`Cannot find row ${index}`);
}
const cells = ui.cell.getAll(row);
expect(cells[0]).toHaveTextContent(date);
expect(cells[1]).toHaveTextContent(user);
expect(cells[2]).toHaveTextContent(action);
if (rule !== null) {
expect(cells[3]).toHaveTextContent(rule);
}
for (const update of updates) {
expect(cells[4]).toHaveTextContent(update);
}
},
};

beforeEach(() => {
@@ -55,26 +80,33 @@ afterEach(() => {

it('should see the changelog', async () => {
const user = userEvent.setup();

renderChangeLog();

const rows = await ui.row.findAll();
expect(rows).toHaveLength(6);
expect(ui.emptyPage.query()).not.toBeInTheDocument();
expect(rows[1]).toHaveTextContent('May 23, 2019');
expect(rows[1]).not.toHaveTextContent('quality_profiles.severity');
expect(rows[2]).toHaveTextContent('April 23, 2019');
expect(rows[2]).toHaveTextContent(
'Systemquality_profiles.changelog.DEACTIVATEDRule 0quality_profiles.severity_set_to severity.MAJOR',
ui.checkRow(1, 'May 23, 2019', 'System', 'quality_profiles.changelog.ACTIVATED', 'Rule 0');
ui.checkRow(
2,
'April 23, 2019',
'System',
'quality_profiles.changelog.DEACTIVATED',
'Rule 0issue.clean_code_attribute_category.RESPONSIBLE.title_shortissue.software_quality.MAINTAINABILITYissue.software_quality.SECURITY',
[/quality_profiles.severity_set_to severity.MAJOR/],
);
expect(rows[3]).not.toHaveTextContent('April 23, 2019');
expect(rows[3]).not.toHaveTextContent('Systemquality_profiles.changelog.DEACTIVATED');
expect(rows[3]).toHaveTextContent('Rule 1quality_profiles.severity_set_to severity.MAJOR');
expect(rows[4]).toHaveTextContent('John Doe');
expect(rows[4]).not.toHaveTextContent('System');
expect(rows[5]).toHaveTextContent('March 23, 2019');
expect(rows[5]).toHaveTextContent('John Doequality_profiles.changelog.ACTIVATEDRule 2');
expect(rows[5]).toHaveTextContent(
'quality_profiles.severity_set_to severity.CRITICALquality_profiles.parameter_set_to.credentialWords.foo,bar',
ui.checkRow(
3,
'',
'',
'',
'Rule 1issue.clean_code_attribute_category.RESPONSIBLE.title_shortissue.software_quality.MAINTAINABILITYissue.software_quality.SECURITY',
[
/quality_profiles.severity_set_to severity.CRITICAL/,
/quality_profiles.changelog.cca_and_category_changed.*COMPLETE.*INTENTIONAL.*LAWFUL.*RESPONSIBLE/,
/quality_profiles.changelog.impact_added.severity.*MEDIUM.*RELIABILITY/,
/quality_profiles.changelog.impact_removed.severity.HIGH.*MAINTAINABILITY/,
],
);
await user.click(ui.link.get(rows[1]));
expect(screen.getByText('/coding_rules?rule_key=c%3Arule0')).toBeInTheDocument();
@@ -120,10 +152,10 @@ it('should see short changelog for php', async () => {

const rows = await ui.row.findAll();
expect(rows).toHaveLength(2);
expect(rows[1]).toHaveTextContent('May 23, 2019');
expect(rows[1]).toHaveTextContent(
'Systemquality_profiles.changelog.DEACTIVATEDPHP Rulequality_profiles.severity_set_to severity.MAJOR',
);
ui.checkRow(1, 'May 23, 2019', 'System', 'quality_profiles.changelog.DEACTIVATED', 'PHP Rule', [
/quality_profiles.severity_set_to severity.CRITICAL/,
/quality_profiles.changelog.cca_and_category_changed.*COMPLETE.*INTENTIONAL.*CLEAR.*RESPONSIBLE/,
]);
expect(ui.showMore.query()).not.toBeInTheDocument();
});


+ 27
- 1
server/sonar-web/src/main/js/apps/quality-profiles/types.ts View File

@@ -18,6 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Profile as BaseProfile } from '../../api/quality-profiles';
import {
CleanCodeAttribute,
CleanCodeAttributeCategory,
SoftwareImpactSeverity,
SoftwareQuality,
} from '../../types/clean-code-taxonomy';
import { IssueSeverity } from '../../types/issues';
import { Dict } from '../../types/types';

export interface Profile extends BaseProfile {
@@ -31,11 +38,30 @@ export interface Exporter {
languages: string[];
}

export interface ProfileChangelogEventImpactChange {
oldSoftwareQuality?: SoftwareQuality;
newSoftwareQuality?: SoftwareQuality;
oldSeverity?: SoftwareImpactSeverity;
newSeverity?: SoftwareImpactSeverity;
}

export interface ProfileChangelogEvent {
action: string;
authorName?: string;
cleanCodeAttributeCategory?: CleanCodeAttributeCategory;
impacts: {
softwareQuality: SoftwareQuality;
severity: SoftwareImpactSeverity;
}[];
date: string;
params?: Dict<string | null>;
params?: {
severity?: IssueSeverity;
oldCleanCodeAttribute?: CleanCodeAttribute;
oldCleanCodeAttributeCategory?: CleanCodeAttributeCategory;
newCleanCodeAttribute?: CleanCodeAttribute;
newCleanCodeAttributeCategory?: CleanCodeAttributeCategory;
impactChanges?: ProfileChangelogEventImpactChange[];
} & Dict<string | ProfileChangelogEventImpactChange[] | null>;
ruleKey: string;
ruleName: string;
}

+ 12
- 1
server/sonar-web/src/main/js/helpers/testMocks.ts View File

@@ -510,8 +510,19 @@ export function mockQualityProfileChangelogEvent(
action: 'ACTIVATED',
date: '2019-04-23T02:12:32+0100',
params: {
severity: 'MAJOR',
severity: IssueSeverity.Major,
},
cleanCodeAttributeCategory: CleanCodeAttributeCategory.Responsible,
impacts: [
{
softwareQuality: SoftwareQuality.Maintainability,
severity: SoftwareImpactSeverity.Low,
},
{
softwareQuality: SoftwareQuality.Security,
severity: SoftwareImpactSeverity.High,
},
],
ruleKey: 'rule-key',
ruleName: 'rule-name',
...eventOverride,

+ 5
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2016,6 +2016,11 @@ quality_profiles.changelog.ACTIVATED=Activated
quality_profiles.changelog.DEACTIVATED=Deactivated
quality_profiles.changelog.UPDATED=Updated
quality_profiles.changelog.parameter_reset_to_default_value=Parameter {0} reset to default value
quality_profiles.changelog.cca_and_category_changed=Clean Code category set to {newCleanCodeAttributeCategory} and attribute set to {newCleanCodeAttribute}, was {oldCleanCodeAttributeCategory} and {oldCleanCodeAttribute}
quality_profiles.changelog.cca_only_changed=Clean Code attribute set to {newCleanCodeAttribute}, was {oldCleanCodeAttribute}
quality_profiles.changelog.impact_changed=Software impact set to {newSoftwareQuality} with severity {newSeverity}, was {oldSoftwareQuality} with severity {oldSeverity}
quality_profiles.changelog.impact_added=Software impact {newSoftwareQuality} with severity {newSeverity} was added
quality_profiles.changelog.impact_removed=Software impact {oldSoftwareQuality} with severity {oldSeverity} was removed
quality_profiles.deleted_profile=The profile {0} doesn't exist anymore
quality_profiles.projects_for_default=Every project not specifically associated with a quality profile will be associated to this one by default.
quality_profile.x_rules={count} rule(s)

Loading…
Cancel
Save