]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12026 Add new hotspot status facet in issues page
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 6 May 2019 15:12:05 +0000 (17:12 +0200)
committerSonarTech <sonartech@sonarsource.com>
Wed, 22 May 2019 18:21:13 +0000 (20:21 +0200)
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StatusFacet-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StatusFacet-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx
server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap
server/sonar-web/src/main/js/components/icons-components/StatusIcon.tsx
server/sonar-web/src/main/js/components/search-navigator.css
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5e069b27e14a9fc0985763ec08cdbdf174de952a..958245f1ad695f9726fd3061071e097f0513d9d0 100644 (file)
@@ -38,7 +38,7 @@ interface Props {
   stats: T.Dict<number> | undefined;
 }
 
-const RESOLUTIONS = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];
+const RESOLUTIONS = ['', 'FALSE-POSITIVE', 'FIXED', 'REMOVED', 'WONTFIX'];
 
 export default class ResolutionFacet extends React.PureComponent<Props> {
   property = 'resolutions';
index 03d6864947a1bb3f4aeeea7edc6d3643c4e76b45..ea83e7a20c25fd578d4274bf6703c1c7538a1d70 100644 (file)
@@ -38,14 +38,14 @@ interface Props {
   statuses: string[];
 }
 
-const STATUSES = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];
+const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED'];
+const HOTSPOT_STATUSES = ['TO_REVIEW', 'REVIEWED', 'IN_REVIEW'];
+const COMMON_STATUSES = ['CLOSED'];
 
 export default class StatusFacet extends React.PureComponent<Props> {
   property = 'statuses';
 
-  static defaultProps = {
-    open: true
-  };
+  static defaultProps = { open: true };
 
   handleItemClick = (itemValue: string, multiple: boolean) => {
     const { statuses } = this.props;
@@ -110,7 +110,15 @@ export default class StatusFacet extends React.PureComponent<Props> {
         <DeferredSpinner loading={this.props.fetching} />
         {this.props.open && (
           <>
-            <FacetItemsList>{STATUSES.map(this.renderItem)}</FacetItemsList>
+            <FacetItemsList title={translate('issues')}>
+              {STATUSES.map(this.renderItem)}
+            </FacetItemsList>
+            <FacetItemsList title={translate('issue.type.SECURITY_HOTSPOT.plural')}>
+              {HOTSPOT_STATUSES.map(this.renderItem)}
+            </FacetItemsList>
+            <FacetItemsList title={translate('issues.issues_and_hotspots')}>
+              {COMMON_STATUSES.map(this.renderItem)}
+            </FacetItemsList>
             <MultipleSelectionHint options={Object.keys(stats).length} values={statuses.length} />
           </>
         )}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StatusFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StatusFacet-test.tsx
new file mode 100644 (file)
index 0000000..3b7b148
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import StatusFacet from '../StatusFacet';
+import { click } from '../../../../helpers/testUtils';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should toggle status facet', () => {
+  const onToggle = jest.fn();
+  const wrapper = shallowRender({ onToggle });
+  click(wrapper.children('FacetHeader'));
+  expect(onToggle).toBeCalledWith('statuses');
+});
+
+it('should clear status facet', () => {
+  const onChange = jest.fn();
+  const wrapper = shallowRender({ onChange, statuses: ['TO_REVIEW'] });
+  wrapper.children('FacetHeader').prop<Function>('onClear')();
+  expect(onChange).toBeCalledWith({ statuses: [] });
+});
+
+it('should select a status', () => {
+  const onChange = jest.fn();
+  const wrapper = shallowRender({ onChange });
+  clickAndCheck('TO_REVIEW');
+  clickAndCheck('OPEN', true, ['OPEN', 'TO_REVIEW']);
+  clickAndCheck('CONFIRMED');
+
+  function clickAndCheck(status: string, multiple = false, expected = [status]) {
+    wrapper
+      .find(`FacetItemsList`)
+      .find(`FacetItem[value="${status}"]`)
+      .prop<Function>('onClick')(status, multiple);
+    expect(onChange).lastCalledWith({ statuses: expected });
+    wrapper.setProps({ statuses: expected });
+  }
+});
+
+function shallowRender(props: Partial<StatusFacet['props']> = {}) {
+  return shallow(
+    <StatusFacet
+      fetching={false}
+      onChange={jest.fn()}
+      onToggle={jest.fn()}
+      open={true}
+      stats={{
+        OPEN: 104,
+        CONFIRMED: 8,
+        REOPENED: 0,
+        RESOLVED: 0,
+        CLOSED: 8,
+        TO_REVIEW: 150,
+        IN_REVIEW: 7,
+        REVIEWED: 1105
+      }}
+      statuses={[]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StatusFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StatusFacet-test.tsx.snap
new file mode 100644 (file)
index 0000000..9d01da1
--- /dev/null
@@ -0,0 +1,163 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<FacetBox
+  property="statuses"
+>
+  <FacetHeader
+    name="issues.facet.statuses"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={Array []}
+  />
+  <DeferredSpinner
+    loading={false}
+    timeout={100}
+  />
+  <FacetItemsList
+    title="issues"
+  >
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={true}
+      key="OPEN"
+      loading={false}
+      name={
+        <StatusHelper
+          status="OPEN"
+        />
+      }
+      onClick={[Function]}
+      stat="104"
+      tooltip="issue.status.OPEN"
+      value="OPEN"
+    />
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={true}
+      key="CONFIRMED"
+      loading={false}
+      name={
+        <StatusHelper
+          status="CONFIRMED"
+        />
+      }
+      onClick={[Function]}
+      stat="8"
+      tooltip="issue.status.CONFIRMED"
+      value="CONFIRMED"
+    />
+    <FacetItem
+      active={false}
+      disabled={true}
+      halfWidth={true}
+      key="REOPENED"
+      loading={false}
+      name={
+        <StatusHelper
+          status="REOPENED"
+        />
+      }
+      onClick={[Function]}
+      stat={0}
+      tooltip="issue.status.REOPENED"
+      value="REOPENED"
+    />
+    <FacetItem
+      active={false}
+      disabled={true}
+      halfWidth={true}
+      key="RESOLVED"
+      loading={false}
+      name={
+        <StatusHelper
+          status="RESOLVED"
+        />
+      }
+      onClick={[Function]}
+      stat={0}
+      tooltip="issue.status.RESOLVED"
+      value="RESOLVED"
+    />
+  </FacetItemsList>
+  <FacetItemsList
+    title="issue.type.SECURITY_HOTSPOT.plural"
+  >
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={true}
+      key="TO_REVIEW"
+      loading={false}
+      name={
+        <StatusHelper
+          status="TO_REVIEW"
+        />
+      }
+      onClick={[Function]}
+      stat="150"
+      tooltip="issue.status.TO_REVIEW"
+      value="TO_REVIEW"
+    />
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={true}
+      key="REVIEWED"
+      loading={false}
+      name={
+        <StatusHelper
+          status="REVIEWED"
+        />
+      }
+      onClick={[Function]}
+      stat="1.1short_number_suffix.k"
+      tooltip="issue.status.REVIEWED"
+      value="REVIEWED"
+    />
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={true}
+      key="IN_REVIEW"
+      loading={false}
+      name={
+        <StatusHelper
+          status="IN_REVIEW"
+        />
+      }
+      onClick={[Function]}
+      stat="7"
+      tooltip="issue.status.IN_REVIEW"
+      value="IN_REVIEW"
+    />
+  </FacetItemsList>
+  <FacetItemsList
+    title="issues.issues_and_hotspots"
+  >
+    <FacetItem
+      active={false}
+      disabled={false}
+      halfWidth={true}
+      key="CLOSED"
+      loading={false}
+      name={
+        <StatusHelper
+          status="CLOSED"
+        />
+      }
+      onClick={[Function]}
+      stat="8"
+      tooltip="issue.status.CLOSED"
+      value="CLOSED"
+    />
+  </FacetItemsList>
+  <MultipleSelectionHint
+    options={8}
+    values={0}
+  />
+</FacetBox>
+`;
index f72a84915e8bcfba9b6a098baf8bfe7957110736..b58c19d585b63d73feeacb43880c14ebd7965dc6 100644 (file)
@@ -21,8 +21,14 @@ import * as React from 'react';
 
 interface Props {
   children?: React.ReactNode;
+  title?: string;
 }
 
-export default function FacetItemsList(props: Props) {
-  return <div className="search-navigator-facet-list">{props.children}</div>;
+export default function FacetItemsList({ children, title }: Props) {
+  return (
+    <div className="search-navigator-facet-list">
+      {title && <div className="search-navigator-facet-list-title">{title}</div>}
+      {children}
+    </div>
+  );
 }
index 82ebe6d53fd26831d8d91979fe0b16b75fbbf97b..c8f21ca697b3267bd6ee95c4353cc09994941e6f 100644 (file)
@@ -30,3 +30,13 @@ it('should render', () => {
     )
   ).toMatchSnapshot();
 });
+
+it('should render with title', () => {
+  expect(
+    shallow(
+      <FacetItemsList title="title test">
+        <div />
+      </FacetItemsList>
+    )
+  ).toMatchSnapshot();
+});
index 9962cfc364e340f199faf990e6dba997870cff13..fe288787b4396244c651d027c851684519f28760 100644 (file)
@@ -7,3 +7,16 @@ exports[`should render 1`] = `
   <div />
 </div>
 `;
+
+exports[`should render with title 1`] = `
+<div
+  className="search-navigator-facet-list"
+>
+  <div
+    className="search-navigator-facet-list-title"
+  >
+    title test
+  </div>
+  <div />
+</div>
+`;
index a4d0e3bfcb9e683b9158bd45040e58eb7f017f1a..74fd63f4bd11247ea4ed6c82b6d57c21297d5d7c 100644 (file)
@@ -31,7 +31,10 @@ const statusIcons: T.Dict<(props: IconProps) => React.ReactElement<any>> = {
   confirmed: ConfirmedStatusIcon,
   reopened: ReopenedStatusIcon,
   resolved: ResolvedStatusIcon,
-  closed: ClosedStatusIcon
+  closed: ClosedStatusIcon,
+  to_review: OpenStatusIcon,
+  in_review: ConfirmedStatusIcon,
+  reviewed: ResolvedStatusIcon
 };
 
 export default function StatusIcon(props: Props) {
index a723606ffb5c31e5c09e17ac4b15a0e43ff65551..75c8d0f51104d1cd825314b03a065df99dffc216 100644 (file)
@@ -358,6 +358,17 @@ a.search-navigator-facet:focus,
   font-size: 0;
 }
 
+.search-navigator-facet-list-title {
+  margin: 0 var(--gridSize) calc(var(--gridSize) / 2);
+  font-size: var(--smallFontSize);
+  font-weight: bold;
+}
+
+.search-navigator-facet-list + .search-navigator-facet-list > .search-navigator-facet-list-title {
+  border-top: 1px solid var(--barBorderColor);
+  padding-top: var(--gridSize);
+}
+
 .search-navigator-facet-empty {
   margin: 0 0 0 0;
   padding: 0 10px 10px;
index 7c359ebaff4c98824ba8dc842c4c11abf5a3a92f..8ed013ff7836456ce0f3e0162309270202485d0f 100644 (file)
@@ -645,23 +645,19 @@ issue.type.VULNERABILITY.plural=Vulnerabilities
 issue.type.SECURITY_HOTSPOT.plural=Security Hotspots
 
 issue.status.REOPENED=Reopened
-issue.status.REOPENED.description=Transitioned to and then back from some other status.
 issue.status.RESOLVED=Resolved
-issue.status.RESOLVED.description=Manually marked as corrected.
 issue.status.OPEN=Open
-issue.status.OPEN.description=Untouched. This status is set automatically at issue creation.
 issue.status.CONFIRMED=Confirmed
-issue.status.CONFIRMED.description=Manually examined and affirmed as an issue that needs attention.
 issue.status.CLOSED=Closed
-issue.status.CLOSED.description=Non-active and no longer requiring attention.
-issue.status.TOREVIEW=To Review
-issue.status.TOREVIEW.description=A review is required to check for a vulnerability.
+issue.status.TO_REVIEW=To Review
+issue.status.IN_REVIEW=In Review
+issue.status.REVIEWED=Reviewed
 
 issue.resolution.FALSE-POSITIVE=False Positive
 issue.resolution.FALSE-POSITIVE.description=Issues that manual review determined were False Positives. Effort from these issues is ignored.
 issue.resolution.FIXED=Fixed
 issue.resolution.FIXED.description=Issues that were corrected in code and reanalyzed.
-issue.resolution.WONTFIX=Won't fix
+issue.resolution.WONTFIX=Won't Fix
 issue.resolution.WONTFIX.description=Issues that are accepted in this context. They and their effort will be ignored.
 issue.resolution.REMOVED=Removed
 issue.resolution.REMOVED.description=Either the rule or the resource was changed (removed, relocated, parameters changed, etc.) so that analysis no longer finds these issues.
@@ -687,6 +683,7 @@ issues.my_issues=My Issues
 issues.no_my_issues=There are no issues assigned to you.
 issues.no_issues=No Issues. Hooray!
 issues.x_more_locations=+ {0} more location(s)
+issues.issues_and_hotspots=Issues & Security Hotspots
 issues.hotspots.helper=Security Hotspots aren't necessarily issues, but they need to be reviewed to make sure they aren't vulnerabilities.
 
 
@@ -723,6 +720,7 @@ issues.facet.types=Type
 issues.facet.severities=Severity
 issues.facet.projects=Project
 issues.facet.statuses=Status
+issues.facet.hotspotStatuses=Hotspot Status
 issues.facet.assignees=Assignee
 issues.facet.files=File
 issues.facet.modules=Module
@@ -2100,7 +2098,7 @@ projects_role.admin.desc=Access project settings and perform administration task
 projects_role.issueadmin=Administer Issues
 projects_role.issueadmin.desc=Change the type and severity of issues, resolve issues as being "won't fix" or "false-positive" (users also need "Browse" permission).
 projects_role.securityhotspotadmin=Administer Security Hotspots
-projects_role.securityhotspotadmin.desc=Detect a Vulnerability from a Security Hotspot. Reject, clear, accept, reopen a Security Hotspot (users also need "Browse" permissions).
+projects_role.securityhotspotadmin.desc=Open a Vulnerability from a Security Hotspot. Resolved a Security Hotspot as reviewed, set it as in review or reset it as to review (users also need Browse permission).
 projects_role.user=Browse
 projects_role.user.desc=Access a project, browse its measures and issues, confirm or resolve issues as "fixed", change the assignee, comment on issues and change tags.
 projects_role.codeviewer=See Source Code