|
|
@@ -17,22 +17,26 @@ |
|
|
|
* along with this program; if not, write to the Free Software Foundation, |
|
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
|
|
*/ |
|
|
|
import styled from '@emotion/styled'; |
|
|
|
import { |
|
|
|
ButtonPrimary, |
|
|
|
ContentCell, |
|
|
|
NumericalCell, |
|
|
|
Spinner, |
|
|
|
SubTitle, |
|
|
|
Table, |
|
|
|
TableRow, |
|
|
|
} from 'design-system/lib'; |
|
|
|
} from 'design-system'; |
|
|
|
import { keyBy } from 'lodash'; |
|
|
|
import * as React from 'react'; |
|
|
|
import { useEffect, useState } from 'react'; |
|
|
|
import { getQualityProfile } from '../../../api/quality-profiles'; |
|
|
|
import { searchRules } from '../../../api/rules'; |
|
|
|
import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; |
|
|
|
import { translate } from '../../../helpers/l10n'; |
|
|
|
import { isDefined } from '../../../helpers/types'; |
|
|
|
import { getRulesUrl } from '../../../helpers/urls'; |
|
|
|
import { CleanCodeAttributeCategory, SoftwareQuality } from '../../../types/clean-code-taxonomy'; |
|
|
|
import { SearchRulesResponse } from '../../../types/coding-rules'; |
|
|
|
import { Dict } from '../../../types/types'; |
|
|
|
import { Profile } from '../types'; |
|
|
@@ -40,8 +44,6 @@ import ProfileRulesDeprecatedWarning from './ProfileRulesDeprecatedWarning'; |
|
|
|
import ProfileRulesRow from './ProfileRulesRow'; |
|
|
|
import ProfileRulesSonarWayComparison from './ProfileRulesSonarWayComparison'; |
|
|
|
|
|
|
|
const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT']; |
|
|
|
|
|
|
|
interface Props { |
|
|
|
profile: Profile; |
|
|
|
} |
|
|
@@ -51,172 +53,181 @@ interface ByType { |
|
|
|
count: number | null; |
|
|
|
} |
|
|
|
|
|
|
|
interface State { |
|
|
|
activatedTotal: number | null; |
|
|
|
activatedByType: Dict<ByType>; |
|
|
|
allByType: Dict<ByType>; |
|
|
|
compareToSonarWay: { profile: string; profileName: string; missingRuleCount: number } | null; |
|
|
|
total: number | null; |
|
|
|
} |
|
|
|
|
|
|
|
export default class ProfileRules extends React.PureComponent<Readonly<Props>, State> { |
|
|
|
mounted = false; |
|
|
|
|
|
|
|
state: State = { |
|
|
|
activatedTotal: null, |
|
|
|
activatedByType: keyBy( |
|
|
|
TYPES.map((t) => ({ val: t, count: null })), |
|
|
|
'val', |
|
|
|
), |
|
|
|
allByType: keyBy( |
|
|
|
TYPES.map((t) => ({ val: t, count: null })), |
|
|
|
'val', |
|
|
|
), |
|
|
|
compareToSonarWay: null, |
|
|
|
total: null, |
|
|
|
}; |
|
|
|
|
|
|
|
componentDidMount() { |
|
|
|
this.mounted = true; |
|
|
|
this.loadRules(); |
|
|
|
} |
|
|
|
|
|
|
|
componentDidUpdate(prevProps: Props) { |
|
|
|
if (prevProps.profile.key !== this.props.profile.key) { |
|
|
|
this.loadRules(); |
|
|
|
export default function ProfileRules({ profile }: Readonly<Props>) { |
|
|
|
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<{ |
|
|
|
profile: string; |
|
|
|
profileName: string; |
|
|
|
missingRuleCount: number; |
|
|
|
} | null>(null); |
|
|
|
|
|
|
|
const loadRules = React.useCallback(async () => { |
|
|
|
function findFacet(response: SearchRulesResponse, property: string) { |
|
|
|
const facet = response.facets?.find((f) => f.property === property); |
|
|
|
return facet ? facet.values : []; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
componentWillUnmount() { |
|
|
|
this.mounted = false; |
|
|
|
} |
|
|
|
|
|
|
|
loadProfile() { |
|
|
|
if (this.props.profile.isBuiltIn) { |
|
|
|
return Promise.resolve(null); |
|
|
|
try { |
|
|
|
setLoading(true); |
|
|
|
return await Promise.all([ |
|
|
|
searchRules({ |
|
|
|
languages: profile.language, |
|
|
|
facets: 'cleanCodeAttributeCategories,impactSoftwareQualities', |
|
|
|
}), |
|
|
|
searchRules({ |
|
|
|
activation: 'true', |
|
|
|
facets: 'cleanCodeAttributeCategories,impactSoftwareQualities', |
|
|
|
qprofile: profile.key, |
|
|
|
}), |
|
|
|
!profile.isBuiltIn && |
|
|
|
getQualityProfile({ |
|
|
|
compareToSonarWay: true, |
|
|
|
profile, |
|
|
|
}), |
|
|
|
]).then((responses) => { |
|
|
|
const [allRules, activatedRules, showProfile] = responses; |
|
|
|
setTotalByCctCategory( |
|
|
|
keyBy<ByType>(findFacet(allRules, 'cleanCodeAttributeCategories'), 'val'), |
|
|
|
); |
|
|
|
setCountsByCctCategory( |
|
|
|
keyBy<ByType>(findFacet(activatedRules, 'cleanCodeAttributeCategories'), 'val'), |
|
|
|
); |
|
|
|
setTotalBySoftwareQuality( |
|
|
|
keyBy<ByType>(findFacet(allRules, 'impactSoftwareQualities'), 'val'), |
|
|
|
); |
|
|
|
setCountsBySoftwareImpact( |
|
|
|
keyBy<ByType>(findFacet(activatedRules, 'impactSoftwareQualities'), 'val'), |
|
|
|
); |
|
|
|
setSonarWayDiff(showProfile?.compareToSonarWay); |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setLoading(false); |
|
|
|
} |
|
|
|
return getQualityProfile({ |
|
|
|
compareToSonarWay: true, |
|
|
|
profile: this.props.profile, |
|
|
|
}); |
|
|
|
} |
|
|
|
}, [profile]); |
|
|
|
|
|
|
|
loadAllRules() { |
|
|
|
return searchRules({ |
|
|
|
languages: this.props.profile.language, |
|
|
|
facets: 'types', |
|
|
|
ps: 1, |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
loadActivatedRules() { |
|
|
|
return searchRules({ |
|
|
|
activation: 'true', |
|
|
|
facets: 'types', |
|
|
|
ps: 1, |
|
|
|
qprofile: this.props.profile.key, |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
loadRules() { |
|
|
|
return Promise.all([this.loadAllRules(), this.loadActivatedRules(), this.loadProfile()]).then( |
|
|
|
(responses) => { |
|
|
|
if (this.mounted) { |
|
|
|
const [allRules, activatedRules, showProfile] = responses; |
|
|
|
this.setState({ |
|
|
|
activatedTotal: activatedRules.paging.total, |
|
|
|
allByType: keyBy<ByType>(this.takeFacet(allRules, 'types'), 'val'), |
|
|
|
activatedByType: keyBy<ByType>(this.takeFacet(activatedRules, 'types'), 'val'), |
|
|
|
compareToSonarWay: showProfile?.compareToSonarWay, |
|
|
|
total: allRules.paging.total, |
|
|
|
}); |
|
|
|
} |
|
|
|
}, |
|
|
|
); |
|
|
|
} |
|
|
|
useEffect(() => { |
|
|
|
loadRules(); |
|
|
|
}, [profile.key, loadRules]); |
|
|
|
|
|
|
|
takeFacet(response: SearchRulesResponse, property: string) { |
|
|
|
const facet = response.facets?.find((f) => f.property === property); |
|
|
|
return facet ? facet.values : []; |
|
|
|
if (loading) { |
|
|
|
return <Spinner />; |
|
|
|
} |
|
|
|
|
|
|
|
render() { |
|
|
|
const { profile } = this.props; |
|
|
|
const { compareToSonarWay } = this.state; |
|
|
|
const activateMoreUrl = getRulesUrl({ qprofile: profile.key, activation: 'false' }); |
|
|
|
const { actions = {} } = profile; |
|
|
|
return ( |
|
|
|
<section aria-label={translate('rules')} className="it__quality-profiles__rules"> |
|
|
|
<SubTitle>{translate('quality_profile.rules.breakdown')}</SubTitle> |
|
|
|
|
|
|
|
return ( |
|
|
|
<section aria-label={translate('rules')} className="it__quality-profiles__rules"> |
|
|
|
<StyledTableContainer> |
|
|
|
<Table |
|
|
|
columnCount={3} |
|
|
|
columnWidths={['50%', '25%', '25%']} |
|
|
|
header={ |
|
|
|
<TableRow> |
|
|
|
<ContentCell> |
|
|
|
<SubTitle className="sw-mb-0">{translate('rules')}</SubTitle> |
|
|
|
<StyledTableRowHeader> |
|
|
|
<ContentCell className="sw-font-semibold sw-pl-4"> |
|
|
|
{translate('quality_profile.rules.cct_categories_title')} |
|
|
|
</ContentCell> |
|
|
|
<NumericalCell>{translate('active')}</NumericalCell> |
|
|
|
<NumericalCell>{translate('inactive')}</NumericalCell> |
|
|
|
</TableRow> |
|
|
|
<NumericalCell className="sw-pr-4">{translate('inactive')}</NumericalCell> |
|
|
|
</StyledTableRowHeader> |
|
|
|
} |
|
|
|
noHeaderTopBorder |
|
|
|
noSidePadding |
|
|
|
> |
|
|
|
<ProfileRulesRow |
|
|
|
className="it__quality-profiles__rules__total" |
|
|
|
count={this.state.activatedTotal} |
|
|
|
qprofile={profile.key} |
|
|
|
total={this.state.total} |
|
|
|
/> |
|
|
|
{TYPES.map((type) => ( |
|
|
|
{Object.values(CleanCodeAttributeCategory).map((category) => ( |
|
|
|
<ProfileRulesRow |
|
|
|
count={this.state.activatedByType[type]?.count} |
|
|
|
key={type} |
|
|
|
title={translate('issue.clean_code_attribute_category', category)} |
|
|
|
total={totalByCctCategory[category]?.count} |
|
|
|
count={countsByCctCategory[category]?.count} |
|
|
|
key={category} |
|
|
|
qprofile={profile.key} |
|
|
|
total={this.state.allByType[type]?.count} |
|
|
|
type={type} |
|
|
|
propertyName="cleanCodeAttributeCategories" |
|
|
|
propertyValue={category} |
|
|
|
/> |
|
|
|
))} |
|
|
|
</Table> |
|
|
|
</StyledTableContainer> |
|
|
|
|
|
|
|
<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(compareToSonarWay) && compareToSonarWay.missingRuleCount > 0 && ( |
|
|
|
<ProfileRulesSonarWayComparison |
|
|
|
language={profile.language} |
|
|
|
profile={profile.key} |
|
|
|
sonarWayMissingRules={compareToSonarWay.missingRuleCount} |
|
|
|
sonarway={compareToSonarWay.profile} |
|
|
|
<StyledTableContainer className="sw-mt-4"> |
|
|
|
<Table |
|
|
|
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>{translate('active')}</NumericalCell> |
|
|
|
<NumericalCell className="sw-pr-4">{translate('inactive')}</NumericalCell> |
|
|
|
</StyledTableRowHeader> |
|
|
|
} |
|
|
|
noHeaderTopBorder |
|
|
|
noSidePadding |
|
|
|
> |
|
|
|
{Object.values(SoftwareQuality).map((quality) => ( |
|
|
|
<ProfileRulesRow |
|
|
|
title={translate('issue.software_quality', quality)} |
|
|
|
total={totalBySoftwareQuality[quality]?.count} |
|
|
|
count={countsBySoftwareImpact[quality]?.count} |
|
|
|
key={quality} |
|
|
|
qprofile={profile.key} |
|
|
|
propertyName="impactSoftwareQualities" |
|
|
|
propertyValue={quality} |
|
|
|
/> |
|
|
|
)} |
|
|
|
))} |
|
|
|
</Table> |
|
|
|
</StyledTableContainer> |
|
|
|
|
|
|
|
{actions.edit && !profile.isBuiltIn && ( |
|
|
|
<ButtonPrimary className="it__quality-profiles__activate-rules" to={activateMoreUrl}> |
|
|
|
<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> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* 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 && ( |
|
|
|
<DocumentationTooltip content={translate('quality_profiles.activate_more.help.built_in')}> |
|
|
|
<ButtonPrimary className="it__quality-profiles__activate-rules" disabled> |
|
|
|
{translate('quality_profiles.activate_more')} |
|
|
|
</ButtonPrimary> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* 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 && ( |
|
|
|
<DocumentationTooltip |
|
|
|
content={translate('quality_profiles.activate_more.help.built_in')} |
|
|
|
> |
|
|
|
<ButtonPrimary className="it__quality-profiles__activate-rules" disabled> |
|
|
|
{translate('quality_profiles.activate_more')} |
|
|
|
</ButtonPrimary> |
|
|
|
</DocumentationTooltip> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
); |
|
|
|
} |
|
|
|
</DocumentationTooltip> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const StyledTableContainer = styled.div` |
|
|
|
border-radius: 4px; |
|
|
|
border: 1px solid #dddddd; |
|
|
|
`; |
|
|
|
|
|
|
|
const StyledTableRowHeader = styled(TableRow)` |
|
|
|
border-radius: 3px; |
|
|
|
background-color: #eff2f9; |
|
|
|
`; |