@@ -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', | |||
}, | |||
}), |
@@ -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> |
@@ -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> |
@@ -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> | |||
); | |||
} |
@@ -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; | |||
} |
@@ -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(); | |||
}); | |||
@@ -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; | |||
} |
@@ -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, |
@@ -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) |