]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9067 Display multiple flows in the issues list (#1969)
authorStas Vilchik <stas-vilchik@users.noreply.github.com>
Mon, 24 Apr 2017 08:36:38 +0000 (10:36 +0200)
committerGitHub <noreply@github.com>
Mon, 24 Apr 2017 08:36:38 +0000 (10:36 +0200)
16 files changed:
server/sonar-web/src/main/js/apps/issues/actions.js
server/sonar-web/src/main/js/apps/issues/components/App.js
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap
server/sonar-web/src/main/js/apps/issues/styles.css
server/sonar-web/src/main/js/components/common/LocationIndex.js
server/sonar-web/src/main/js/helpers/issues.js

index 69bc8192302f33aa35422981002bb819639a1161..610628b15ff6fa9de157aafcbacd035ae91d047e 100644 (file)
@@ -22,6 +22,8 @@ import type { State } from './components/App';
 
 export const enableLocationsNavigator = (state: State) => ({
   locationsNavigator: true,
+  selectedFlowIndex: state.selectedFlowIndex ||
+    (state.openIssue && state.openIssue.flows.length > 0 ? 0 : null),
   selectedLocationIndex: state.selectedLocationIndex || 0
 });
 
@@ -47,12 +49,13 @@ export const selectLocation = (nextIndex: ?number) => (state: State) => {
 };
 
 export const selectNextLocation = (state: State) => {
-  const { selectedLocationIndex: index, openIssue } = state;
+  const { selectedFlowIndex, selectedLocationIndex: index, openIssue } = state;
   if (openIssue) {
+    const locations = selectedFlowIndex != null
+      ? openIssue.flows[selectedFlowIndex]
+      : openIssue.secondaryLocations;
     return {
-      selectedLocationIndex: index != null && openIssue.secondaryLocations.length > index + 1
-        ? index + 1
-        : index
+      selectedLocationIndex: index != null && locations.length > index + 1 ? index + 1 : index
     };
   }
 };
@@ -63,3 +66,7 @@ export const selectPreviousLocation = (state: State) => {
     return { selectedLocationIndex: index != null && index > 0 ? index - 1 : index };
   }
 };
+
+export const selectFlow = (nextIndex: ?number) => () => {
+  return { selectedFlowIndex: nextIndex, selectedLocationIndex: 0 };
+};
index 4fac9bbe264b3ba8a8936a129fd44fc241422998..1b7f6a9f9522d2778598971de69879b8286c6721 100644 (file)
@@ -89,6 +89,7 @@ export type State = {
   referencedRules: { [string]: { name: string } },
   referencedUsers: { [string]: ReferencedUser },
   selected?: string,
+  selectedFlowIndex: ?number,
   selectedLocationIndex: ?number
 };
 
@@ -117,6 +118,7 @@ export default class App extends React.PureComponent {
       referencedRules: {},
       referencedUsers: {},
       selected: getOpen(props.location.query),
+      selectedFlowIndex: null,
       selectedLocationIndex: null
     };
   }
@@ -137,11 +139,15 @@ export default class App extends React.PureComponent {
     const openIssue = this.getOpenIssue(nextProps, this.state.issues);
 
     if (openIssue != null && openIssue.key !== this.state.selected) {
-      this.setState({ selected: openIssue.key, selectedLocationIndex: null });
+      this.setState({
+        selected: openIssue.key,
+        selectedFlowIndex: null,
+        selectedLocationIndex: null
+      });
     }
 
     if (openIssue == null) {
-      this.setState({ selectedLocationIndex: null });
+      this.setState({ selectedFlowIndex: null, selectedLocationIndex: null });
     }
 
     this.setState({
@@ -252,7 +258,11 @@ export default class App extends React.PureComponent {
       if (this.state.openIssue) {
         this.openIssue(issues[selectedIndex + 1].key);
       } else {
-        this.setState({ selected: issues[selectedIndex + 1].key, selectedLocationIndex: null });
+        this.setState({
+          selected: issues[selectedIndex + 1].key,
+          selectedFlowIndex: null,
+          selectedLocationIndex: null
+        });
       }
     }
   };
@@ -264,7 +274,11 @@ export default class App extends React.PureComponent {
       if (this.state.openIssue) {
         this.openIssue(issues[selectedIndex - 1].key);
       } else {
-        this.setState({ selected: issues[selectedIndex - 1].key, selectedLocationIndex: null });
+        this.setState({
+          selected: issues[selectedIndex - 1].key,
+          selectedFlowIndex: null,
+          selectedLocationIndex: null
+        });
       }
     }
   };
@@ -372,6 +386,7 @@ export default class App extends React.PureComponent {
           selected: issues.length > 0
             ? openIssue != null ? openIssue.key : issues[0].key
             : undefined,
+          selectedFlowIndex: null,
           selectedLocationIndex: null
         });
       }
@@ -560,6 +575,7 @@ export default class App extends React.PureComponent {
   selectLocation = (index: ?number) => this.setState(actions.selectLocation(index));
   selectNextLocation = () => this.setState(actions.selectNextLocation);
   selectPreviousLocation = () => this.setState(actions.selectPreviousLocation);
+  selectFlow = (index: ?number) => this.setState(actions.selectFlow(index));
 
   renderBulkChange(openIssue: ?Issue) {
     const { component, currentUser } = this.props;
@@ -649,9 +665,11 @@ export default class App extends React.PureComponent {
         />
         <ConciseIssuesList
           issues={issues}
+          onFlowSelect={this.selectFlow}
           onIssueSelect={this.openIssue}
           onLocationSelect={this.selectLocation}
           selected={this.state.selected}
+          selectedFlowIndex={this.state.selectedFlowIndex}
           selectedLocationIndex={this.state.selectedLocationIndex}
         />
         {paging != null &&
@@ -755,6 +773,7 @@ export default class App extends React.PureComponent {
                   onIssueChange={this.handleIssueChange}
                   onIssueSelect={this.openIssue}
                   onLocationSelect={this.selectLocation}
+                  selectedFlowIndex={this.state.selectedFlowIndex}
                   selectedLocationIndex={
                     this.state.locationsNavigator ? this.state.selectedLocationIndex : null
                   }
index dc1de95566474a7f396315f0a8eb0def9415c4f1..9c321fb007c4f2e44c47ff5cdbf9996b0fd8a66e 100644 (file)
@@ -29,6 +29,7 @@ type Props = {|
   onIssueSelect: string => void,
   onLocationSelect: number => void,
   openIssue: Issue,
+  selectedFlowIndex: ?number,
   selectedLocationIndex: ?number
 |};
 
@@ -58,9 +59,11 @@ export default class IssuesSourceViewer extends React.PureComponent {
   };
 
   render() {
-    const { openIssue, selectedLocationIndex } = this.props;
+    const { openIssue, selectedFlowIndex, selectedLocationIndex } = this.props;
 
-    const locations = openIssue.secondaryLocations;
+    const locations = selectedFlowIndex != null
+      ? openIssue.flows[selectedFlowIndex]
+      : openIssue.flows.length > 0 ? openIssue.flows[0] : openIssue.secondaryLocations;
 
     const locationMessage = locations != null &&
       selectedLocationIndex != null &&
index d6c1662c2e6868ee3b220838f0279ec81bc7c0ea..5fdeec3da536b9492849849e163e1d6a96ba2889 100644 (file)
@@ -25,11 +25,13 @@ import type { Issue } from '../../../components/issue/types';
 
 type Props = {|
   issue: Issue,
+  onFlowSelect: number => void,
   onLocationSelect: number => void,
   onSelect: string => void,
   previousIssue: ?Issue,
   scroll: HTMLElement => void,
   selected: boolean,
+  selectedFlowIndex: ?number,
   selectedLocationIndex: ?number
 |};
 
@@ -47,9 +49,11 @@ export default class ConciseIssue extends React.PureComponent {
         <ConciseIssueBox
           issue={issue}
           onClick={this.props.onSelect}
+          onFlowSelect={this.props.onFlowSelect}
           onLocationSelect={this.props.onLocationSelect}
           scroll={this.props.scroll}
           selected={selected}
+          selectedFlowIndex={selected ? this.props.selectedFlowIndex : null}
           selectedLocationIndex={selected ? this.props.selectedLocationIndex : null}
         />
       </div>
index da7f573a0622c95267e9b29b4c6f51352e22bcb4..bff17414951f3f12aed90ac4bae9c3940a14e550 100644 (file)
@@ -29,9 +29,11 @@ import type { Issue } from '../../../components/issue/types';
 type Props = {|
   issue: Issue,
   onClick: string => void,
+  onFlowSelect: number => void,
   onLocationSelect: number => void,
   scroll: HTMLElement => void,
   selected: boolean,
+  selectedFlowIndex: ?number,
   selectedLocationIndex: ?number
 |};
 
@@ -66,20 +68,27 @@ export default class ConciseIssueBox extends React.PureComponent {
       : { onClick: this.handleClick, role: 'listitem', tabIndex: 0 };
 
     return (
-      <div className={classNames('concise-issue-box', { selected })} {...clickAttributes}>
+      <div
+        className={classNames('concise-issue-box', 'clearfix', { selected })}
+        {...clickAttributes}>
         <div className="concise-issue-box-message" ref={node => (this.node = node)}>
           {issue.message}
         </div>
         <div className="concise-issue-box-attributes">
           <TypeHelper type={issue.type} />
           <SeverityHelper className="big-spacer-left" severity={issue.severity} />
-          <ConciseIssueLocations issue={issue} />
+          <ConciseIssueLocations
+            issue={issue}
+            onFlowSelect={this.props.onFlowSelect}
+            selectedFlowIndex={this.props.selectedFlowIndex}
+          />
         </div>
         {selected &&
           <ConciseIssueLocationsNavigator
             issue={issue}
             onLocationSelect={this.props.onLocationSelect}
             scroll={this.props.scroll}
+            selectedFlowIndex={this.props.selectedFlowIndex}
             selectedLocationIndex={this.props.selectedLocationIndex}
           />}
       </div>
index 0eb9223eafecb492a42fa14a802e3aba81d3d970..ae19b8f367f5d15b0697766d4e548830b6148ac4 100644 (file)
@@ -25,17 +25,20 @@ import { translateWithParameters } from '../../../helpers/l10n';
 import { formatMeasure } from '../../../helpers/measures';
 
 type Props = {|
-  count: number
+  count: number,
+  onClick?: () => void,
+  selected?: boolean
 |};
 
 export default function ConciseIssueLocationBadge(props: Props) {
   return (
     <Tooltip
+      mouseEnterDelay={0.5}
       overlay={translateWithParameters(
         'issue.this_issue_involves_x_code_locations',
         formatMeasure(props.count)
       )}>
-      <LocationIndex>
+      <LocationIndex onClick={props.onClick} selected={props.selected}>
         {'+'}{props.count}
       </LocationIndex>
     </Tooltip>
index e3563ade3d917c7dece47e4bb1c959b11b30c5b0..cf1144a24bc6223565d08063c598920f4348d680 100644 (file)
@@ -23,22 +23,67 @@ import ConciseIssueLocationBadge from './ConciseIssueLocationBadge';
 import type { Issue } from '../../../components/issue/types';
 
 type Props = {|
-  issue: Issue
+  issue: Issue,
+  onFlowSelect: number => void,
+  selectedFlowIndex: ?number
 |};
 
+type State = {
+  collapsed: boolean
+};
+
+const LIMIT = 3;
+
 export default class ConciseIssueLocations extends React.PureComponent {
   props: Props;
+  state: State = { collapsed: true };
+
+  handleExpandClick = (event: Event) => {
+    event.preventDefault();
+    this.setState({ collapsed: false });
+  };
+
+  renderExpandButton() {
+    return (
+      <a className="little-spacer-left link-no-underline" href="#" onClick={this.handleExpandClick}>
+        ...
+      </a>
+    );
+  }
 
   render() {
     const { secondaryLocations, flows } = this.props.issue;
 
-    return (
-      <div className="pull-right">
-        {secondaryLocations.length > 0 &&
-          <ConciseIssueLocationBadge count={secondaryLocations.length} />}
+    const badges = [];
 
-        {flows.map((flow, index) => <ConciseIssueLocationBadge key={index} count={flow.length} />)}
-      </div>
-    );
+    if (secondaryLocations.length > 0) {
+      badges.push(
+        <ConciseIssueLocationBadge
+          key="-1"
+          count={secondaryLocations.length}
+          selected={this.props.selectedFlowIndex == null}
+        />
+      );
+    }
+
+    flows.forEach((flow, index) => {
+      badges.push(
+        <ConciseIssueLocationBadge
+          key={index}
+          count={flow.length}
+          onClick={() => this.props.onFlowSelect(index)}
+          selected={index === this.props.selectedFlowIndex}
+        />
+      );
+    });
+
+    return this.state.collapsed
+      ? <div className="concise-issue-locations pull-right">
+          {badges.slice(0, LIMIT)}
+          {badges.length > LIMIT && this.renderExpandButton()}
+        </div>
+      : <div className="concise-issue-locations spacer-top">
+          {badges}
+        </div>;
   }
 }
index 7bac75934842738bdddfec17aedaddae9780a3ff..ebe8aff7935c2d603862b7419874498f7cb52267 100644 (file)
@@ -26,6 +26,7 @@ type Props = {|
   issue: Issue,
   onLocationSelect: number => void,
   scroll: HTMLElement => void,
+  selectedFlowIndex: ?number,
   selectedLocationIndex: ?number
 |};
 
@@ -38,16 +39,20 @@ export default class ConciseIssueLocationsNavigator extends React.PureComponent
   };
 
   render() {
-    const { selectedLocationIndex } = this.props;
-    const { secondaryLocations } = this.props.issue;
+    const { selectedFlowIndex, selectedLocationIndex } = this.props;
+    const { flows, secondaryLocations } = this.props.issue;
 
-    if (secondaryLocations.length === 0) {
+    const locations = selectedFlowIndex != null
+      ? flows[selectedFlowIndex]
+      : flows.length > 0 ? flows[0] : secondaryLocations;
+
+    if (locations == null || locations.length === 0) {
       return null;
     }
 
     return (
       <div className="spacer-top">
-        {secondaryLocations.map((location, index) => (
+        {locations.map((location, index) => (
           <ConciseIssueLocationsNavigatorLocation
             key={index}
             index={index}
index e76a1ccdcc09764ed8f2a4654619d9d001369a24..099e7804d2c4b588adff3ffbddbe3249e0557c3f 100644 (file)
@@ -54,7 +54,10 @@ export default class ConciseIssueLocationsNavigatorLocation extends React.PureCo
   render() {
     return (
       <div className="little-spacer-top" ref={node => (this.node = node)}>
-        <a className="link-no-underline" href="#" onClick={this.handleClick}>
+        <a
+          className="consice-issue-locations-navigator-location"
+          href="#"
+          onClick={this.handleClick}>
           <LocationIndex selected={this.props.selected}>
             {this.props.index + 1}
           </LocationIndex>
index 8eae076068699ba73e033cc3660d348b6f96eeca..18e9018a6f5a344d178f89277b73c272bf8af966 100644 (file)
@@ -25,9 +25,11 @@ import type { Issue } from '../../../components/issue/types';
 
 type Props = {|
   issues: Array<Issue>,
+  onFlowSelect: number => void,
   onIssueSelect: string => void,
   onLocationSelect: number => void,
   selected?: string,
+  selectedFlowIndex: ?number,
   selectedLocationIndex: ?number
 |};
 
@@ -48,11 +50,13 @@ export default class ConciseIssuesList extends React.PureComponent {
           <ConciseIssue
             key={issue.key}
             issue={issue}
+            onFlowSelect={this.props.onFlowSelect}
             onLocationSelect={this.props.onLocationSelect}
             onSelect={this.props.onIssueSelect}
             previousIssue={index > 0 ? this.props.issues[index - 1] : null}
             scroll={this.handleScroll}
             selected={issue.key === this.props.selected}
+            selectedFlowIndex={this.props.selectedFlowIndex}
             selectedLocationIndex={this.props.selectedLocationIndex}
           />
         ))}
index 1d0309673f0a6c0ea039a30dead32961173141a2..1413fcc82bf9e4b62ada944a02ffdb81d6286fef 100644 (file)
@@ -5,6 +5,7 @@ exports[`test should render 1`] = `
     issue={Object {}}
     onClick={[Function]}
     selected={false}
+    selectedFlowIndex={null}
     selectedLocationIndex={null} />
 </div>
 `;
index 073f3d922c1cfea2cbe3d284dced43d97bbd7cdf..90378b33f254d35ace21bbb53d3f46d972f1e281 100644 (file)
@@ -1,27 +1,36 @@
 exports[`test should render one flow 1`] = `
 <div
-  className="pull-right">
+  className="concise-issue-locations pull-right">
   <ConciseIssueLocationBadge
-    count={3} />
+    count={3}
+    onClick={[Function]}
+    selected={false} />
 </div>
 `;
 
 exports[`test should render secondary locations 1`] = `
 <div
-  className="pull-right">
+  className="concise-issue-locations pull-right">
   <ConciseIssueLocationBadge
-    count={3} />
+    count={3}
+    selected={true} />
 </div>
 `;
 
 exports[`test should render several flows 1`] = `
 <div
-  className="pull-right">
+  className="concise-issue-locations pull-right">
   <ConciseIssueLocationBadge
-    count={3} />
+    count={3}
+    onClick={[Function]}
+    selected={false} />
   <ConciseIssueLocationBadge
-    count={2} />
+    count={2}
+    onClick={[Function]}
+    selected={false} />
   <ConciseIssueLocationBadge
-    count={3} />
+    count={3}
+    onClick={[Function]}
+    selected={false} />
 </div>
 `;
index 6fbcb9b4e73b142e25c4ef9c9e5061655581e9fd..d035084daef3b6c3390d2fc5687fa3b2dbc5c69f 100644 (file)
 
 .concise-issue-box:not(.selected) .location-index {
   background-color: #ccc;
+}
+
+.concise-issue-locations {
+  margin-right: -4px;
+  margin-bottom: -4px;
+}
+
+.concise-issue-locations .location-index {
+  margin-right: 4px;
+  margin-bottom: 4px;
+}
+
+.consice-issue-locations-navigator-location {
+  display: flex;
+  align-items: flex-start;
+  border: none;
 }
\ No newline at end of file
index 03b74ee0c8440fcba53603729811b7fadd3df641..ecc4027b124346283f9f3cb81cfe404100679576 100644 (file)
@@ -29,19 +29,13 @@ type Props = {
 };
 
 export default function LocationIndex(props: Props) {
-  const clickAttributes = props.onClick
-    ? {
-        onClick: props.onClick,
-        role: 'button',
-        tabIndex: 0
-      }
-    : {};
+  const { children, onClick, selected, ...other } = props;
+  const clickAttributes = onClick ? { onClick, role: 'button', tabIndex: 0 } : {};
 
+  // put {...others} because Tooltip sets some event handlers
   return (
-    <div
-      className={classNames('location-index', { selected: props.selected })}
-      {...clickAttributes}>
-      {props.children}
+    <div className={classNames('location-index', { selected })} {...clickAttributes} {...other}>
+      {children}
     </div>
   );
 }
index 804b410abf000131295233a0e800bf2ce50fb1ed..92603e52e17cdc948efdaa5275545836f9574fa0 100644 (file)
@@ -108,6 +108,12 @@ const ensureTextRange = (issue: RawIssue) => {
     : {};
 };
 
+const reverseLocations = (locations: Array<*>) => {
+  const x = [...locations];
+  x.reverse();
+  return x;
+};
+
 const splitFlows = (
   issue: RawIssue
   // $FlowFixMe textRange is not null
@@ -121,7 +127,7 @@ const splitFlows = (
 
   return onlySecondaryLocations
     ? { secondaryLocations: flatten(parsedFlows), flows: [] }
-    : { secondaryLocations: [], flows: parsedFlows };
+    : { secondaryLocations: [], flows: parsedFlows.map(reverseLocations) };
 };
 
 export const parseIssueFromResponse = (