diff options
-rw-r--r-- | server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx | 78 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx | 49 |
2 files changed, 122 insertions, 5 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx index 6f1debef576..d107b97a5dd 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx @@ -24,6 +24,7 @@ import FacetHeader from '../../../components/facet/FacetHeader'; import FacetItem from '../../../components/facet/FacetItem'; import FacetItemsList from '../../../components/facet/FacetItemsList'; import ListStyleFacet from '../../../components/facet/ListStyleFacet'; +import ListStyleFacetFooter from '../../../components/facet/ListStyleFacetFooter'; import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; import { translate } from '../../../helpers/l10n'; import { highlightTerm } from '../../../helpers/search'; @@ -70,6 +71,7 @@ interface Props { interface State { standards: Standards; + showFullSonarSourceList: boolean; } type StatsProp = @@ -80,10 +82,12 @@ type StatsProp = | 'sonarsourceSecurityStats'; type ValuesProp = StandardType; +const INITIAL_FACET_COUNT = 15; export default class StandardFacet extends React.PureComponent<Props, State> { mounted = false; property = STANDARDS; state: State = { + showFullSonarSourceList: false, standards: { owaspTop10: {}, 'owaspTop10-2021': {}, @@ -327,11 +331,75 @@ export default class StandardFacet extends React.PureComponent<Props, State> { } renderSonarSourceSecurityList() { - return this.renderList( - 'sonarsourceSecurityStats', - SecurityStandard.SONARSOURCE, - renderSonarSourceSecurityCategory, - this.handleSonarSourceSecurityItemClick + const stats = this.props.sonarsourceSecurityStats; + const values = this.props.sonarsourceSecurity; + + if (!stats) { + return null; + } + + const sortedItems = sortBy( + Object.keys(stats), + key => -stats[key], + key => renderSonarSourceSecurityCategory(this.state.standards, key) + ); + + const limitedList = this.state.showFullSonarSourceList + ? sortedItems + : sortedItems.slice(0, INITIAL_FACET_COUNT); + + // make sure all selected items are displayed + const selectedBelowLimit = this.state.showFullSonarSourceList + ? [] + : sortedItems.slice(INITIAL_FACET_COUNT).filter(item => values.includes(item)); + + const allItemShown = limitedList.length + selectedBelowLimit.length === sortedItems.length; + return ( + <> + <FacetItemsList> + {limitedList.map(item => ( + <FacetItem + active={values.includes(item)} + key={item} + name={renderSonarSourceSecurityCategory(this.state.standards, item)} + onClick={this.handleSonarSourceSecurityItemClick} + stat={formatFacetStat(stats[item])} + tooltip={renderSonarSourceSecurityCategory(this.state.standards, item)} + value={item} + /> + ))} + </FacetItemsList> + {selectedBelowLimit.length > 0 && ( + <> + {!allItemShown && <div className="note spacer-bottom text-center">⋯</div>} + <FacetItemsList> + {selectedBelowLimit.map(item => ( + <FacetItem + active={true} + key={item} + name={renderSonarSourceSecurityCategory(this.state.standards, item)} + onClick={this.handleSonarSourceSecurityItemClick} + stat={formatFacetStat(stats[item])} + tooltip={renderSonarSourceSecurityCategory(this.state.standards, item)} + value={item} + /> + ))} + </FacetItemsList> + </> + )} + {!allItemShown && ( + <ListStyleFacetFooter + count={limitedList.length + selectedBelowLimit.length} + showLess={ + this.state.showFullSonarSourceList + ? () => this.setState({ showFullSonarSourceList: false }) + : undefined + } + showMore={() => this.setState({ showFullSonarSourceList: true })} + total={sortedItems.length} + /> + )} + </> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx index a2b9e7cd062..587892576e5 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import ListStyleFacetFooter from '../../../../components/facet/ListStyleFacetFooter'; import { getStandards } from '../../../../helpers/security-standard'; import { click } from '../../../../helpers/testUtils'; import { Query } from '../../utils'; @@ -35,6 +36,14 @@ jest.mock('../../../../helpers/security-standard', () => ({ title: 'Broken Authentication' } }, + 'owaspTop10-2021': { + a1: { + title: 'Injection' + }, + a2: { + title: 'Broken Authentication' + } + }, sansTop25: { 'insecure-interaction': { title: 'Insecure Interaction Between Components' @@ -106,6 +115,46 @@ it('should render sub-facets', () => { expect(getStandards).toBeCalled(); }); +it('should show sonarsource facet more button', () => { + const wrapper = shallowRender({ + open: true, + sonarsourceSecurity: ['traceability', 'permission', 'others'], + sonarsourceSecurityOpen: true, + sonarsourceSecurityStats: { + 'buffer-overflow': 3, + 'sql-injection': 3, + rce: 3, + 'object-injection': 3, + 'command-injection': 3, + 'path-traversal-injection': 3, + 'ldap-injection': 3, + 'xpath-injection': 3, + 'expression-lang-injection': 3, + 'log-injection': 3, + xxe: 3, + xss: 3, + dos: 3, + ssrf: 3, + csrf: 3, + 'http-response-splitting': 3, + 'open-redirect': 3, + 'weak-cryptography': 3, + auth: 3, + 'insecure-conf': 3, + 'file-manipulation': 3, + 'encrypt-data': 3, + traceability: 3, + permission: 3, + others: 3 + } + }); + + expect(wrapper.find(ListStyleFacetFooter).exists()).toBe(true); + + wrapper.setState({ showFullSonarSourceList: true }); + expect(wrapper.find(ListStyleFacetFooter).exists()).toBe(false); +}); + it('should render empty sub-facet', () => { expect( shallowRender({ open: true, sansTop25: [], sansTop25Open: true, sansTop25Stats: {} }).find( |