]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12499 Adding show more on SonarSource security facet
authorMathieu Suen <mathieu.suen@sonarsource.com>
Wed, 30 Mar 2022 14:51:14 +0000 (16:51 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 31 Mar 2022 20:02:59 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx

index 6f1debef57639b75603c98263deef9c9d9c0c953..d107b97a5dd71e7acff5097e14b5366ab29def84 100644 (file)
@@ -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}
+          />
+        )}
+      </>
     );
   }
 
index a2b9e7cd062b9ebb555269412c8390ecedb0b892..587892576e534996177b01423a376870b8502f34 100644 (file)
@@ -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(