]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8550 Group analyses by version in the project activity list
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 23 Jun 2017 09:45:49 +0000 (11:45 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 4 Jul 2017 12:15:34 +0000 (14:15 +0200)
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
server/sonar-web/src/main/js/apps/projectActivity/utils.js
server/sonar-web/src/main/less/init/forms.less
tests/src/test/java/org/sonarqube/pageobjects/ProjectAnalysisItem.java
tests/src/test/java/org/sonarqube/tests/projectEvent/ProjectActivityPageTest.java

index 5242772fef8710a8482131d425f601ed5a184318..e6dd14181e0cfe4926de873beb6b57938b33a355 100644 (file)
  */
 // @flow
 import React from 'react';
-import { groupBy } from 'lodash';
 import moment from 'moment';
 import ProjectActivityAnalysis from './ProjectActivityAnalysis';
 import FormattedDate from '../../../components/ui/FormattedDate';
 import { translate } from '../../../helpers/l10n';
+import { getAnalysesByVersionByDay } from '../utils';
 import type { Analysis } from '../types';
 
 type Props = {
@@ -50,35 +50,44 @@ export default function ProjectActivityAnalysesList(props: Props) {
     );
   }
 
-  const firstAnalysis = props.analyses[0];
-  const byDay = groupBy(props.analyses, analysis => moment(analysis.date).startOf('day').valueOf());
+  const firstAnalysisKey = props.analyses[0].key;
+  const byVersionByDay = getAnalysesByVersionByDay(props.analyses);
   return (
     <div className={props.className}>
-      <ul className="project-activity-days-list">
-        {Object.keys(byDay).map(day => (
-          <li
-            key={day}
-            className="project-activity-day"
-            data-day={moment(Number(day)).format('YYYY-MM-DD')}>
-            <div className="project-activity-date">
-              <FormattedDate date={Number(day)} format="LL" />
-            </div>
-
-            <ul className="project-activity-analyses-list">
-              {byDay[day] != null &&
-                byDay[day].map(analysis => (
-                  <ProjectActivityAnalysis
-                    addCustomEvent={props.addCustomEvent}
-                    addVersion={props.addVersion}
-                    analysis={analysis}
-                    canAdmin={props.canAdmin}
-                    changeEvent={props.changeEvent}
-                    deleteAnalysis={props.deleteAnalysis}
-                    deleteEvent={props.deleteEvent}
-                    isFirst={analysis === firstAnalysis}
-                    key={analysis.key}
-                  />
-                ))}
+      <ul className="project-activity-versions-list">
+        {byVersionByDay.map((version, idx) => (
+          <li key={idx + version.version}>
+            {version.version &&
+              <span className="badge project-activity-version-badge spacer-top big-spacer-bottom">
+                {version.version}
+              </span>}
+            <ul className="project-activity-days-list">
+              {Object.keys(version.byDay).map(day => (
+                <li
+                  key={day}
+                  className="project-activity-day"
+                  data-day={moment(Number(day)).format('YYYY-MM-DD')}>
+                  <div className="project-activity-date">
+                    <FormattedDate date={Number(day)} format="LL" />
+                  </div>
+                  <ul className="project-activity-analyses-list">
+                    {version.byDay[day] != null &&
+                      version.byDay[day].map(analysis => (
+                        <ProjectActivityAnalysis
+                          addCustomEvent={props.addCustomEvent}
+                          addVersion={props.addVersion}
+                          analysis={analysis}
+                          canAdmin={props.canAdmin}
+                          changeEvent={props.changeEvent}
+                          deleteAnalysis={props.deleteAnalysis}
+                          deleteEvent={props.deleteEvent}
+                          isFirst={analysis.key === firstAnalysisKey}
+                          key={analysis.key}
+                        />
+                      ))}
+                  </ul>
+                </li>
+              ))}
             </ul>
           </li>
         ))}
index 870dfd61c6ab50bf4ef7375d7d84be1518eb52d2..406f963f0a354bc259a2f2da6897b384d9669ea6 100644 (file)
@@ -57,7 +57,7 @@ export default function ProjectActivityAnalysis(props: Props) {
         <div className="project-activity-analysis-actions spacer-left">
           <div className="dropdown display-inline-block">
             <button
-              className="js-analysis-actions button-small dropdown-toggle"
+              className="js-analysis-actions button-small button-compact dropdown-toggle"
               data-toggle="dropdown">
               <i className="icon-settings" />
               {' '}
index 5419cf50e102fdf6e6f7e3312ff13b0a35122b5f..1904f8d975d6a93b88de831eef805be1d2d66275 100644 (file)
@@ -63,9 +63,7 @@ export default class ProjectActivityApp extends React.PureComponent {
       nextProps.analyses !== this.props.analyses ||
       activityQueryChanged(this.props.query, nextProps.query)
     ) {
-      this.setState({
-        filteredAnalyses: this.filterAnalyses(nextProps.analyses, nextProps.query)
-      });
+      this.setState({ filteredAnalyses: this.filterAnalyses(nextProps.analyses, nextProps.query) });
     }
   }
 
index d83b4244d6a05e8a545e7eb33035a559a36812cf..92c4436f20d9b64d987fa881e114a06f64272a5f 100644 (file)
@@ -136,7 +136,7 @@ export default class AddEventForm extends React.PureComponent {
 
   render() {
     return (
-      <a className="js-add-event button-small" href="#" onClick={this.openForm}>
+      <a className="js-add-event" href="#" onClick={this.openForm}>
         {translate(this.props.addEventButtonText)}
         {this.state.open && this.renderModal()}
       </a>
index 6754b5dfa125671debf79ba4625d06cc4c1dd788..e27947e03aa3a40958f4d022b3712cd6ac72c791 100644 (file)
@@ -46,7 +46,6 @@
 
 .project-activity-graph {
   flex: 1;
-  max-height: 500px;
 }
 
 .project-activity-graph-legends {
 
 .project-activity-version-badge {
   vertical-align: middle;
-  padding: 4px 8px;
+  padding: 4px 14px 4px 16px;
+  margin-left: -14px;
   border-radius: 2px;
   font-weight: bold;
   font-size: 12px;
index 616633b0a2580961475c11cc401463c37ee59e6f..4375069aad948941f01f060a05de560c54777608 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 // @flow
+import moment from 'moment';
 import {
   cleanQuery,
   parseAsDate,
@@ -26,7 +27,7 @@ import {
   serializeString
 } from '../../helpers/query';
 import { translate } from '../../helpers/l10n';
-import type { MeasureHistory, Query } from './types';
+import type { Analysis, MeasureHistory, Query } from './types';
 import type { RawQuery } from '../../helpers/query';
 
 export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'];
@@ -68,6 +69,30 @@ export const generateCoveredLinesMetric = (
   };
 };
 
+export const getAnalysesByVersionByDay = (
+  analyses: Array<Analysis>
+): Array<{
+  version: ?string,
+  byDay: { [string]: Array<Analysis> }
+}> =>
+  analyses.reduce((acc, analysis) => {
+    if (acc.length === 0) {
+      acc.push({ version: undefined, byDay: {} });
+    }
+    const currentVersion = acc[acc.length - 1];
+    const day = moment(analysis.date).startOf('day').valueOf().toString();
+    if (!currentVersion.byDay[day]) {
+      currentVersion.byDay[day] = [];
+    }
+    currentVersion.byDay[day].push(analysis);
+    const versionEvent = analysis.events.find(event => event.category === 'VERSION');
+    if (versionEvent) {
+      currentVersion.version = versionEvent.name;
+      acc.push({ version: undefined, byDay: {} });
+    }
+    return acc;
+  }, []);
+
 const parseGraph = (value?: string): string => {
   const graph = parseAsString(value);
   return GRAPH_TYPES.includes(graph) ? graph : 'overview';
index 6ef2319bae8d2f9fd793cfafd335c4c9952e8b27..e01aeddf9528925f9f05f146d46b0ce7275a8bd7 100644 (file)
@@ -237,6 +237,10 @@ input[type="submit"].button-grey {
   }
 }
 
+.button-compact {
+  padding: 0 6px;
+}
+
 .button-group {
   display: inline-block;
   vertical-align: middle;
index 2589fa20f02f751cb9fe64bb73f40a30b79459bb..dfd1520b812852fe94e122a65bf1e34900459907 100644 (file)
@@ -39,16 +39,19 @@ public class ProjectAnalysisItem {
   }
 
   public ProjectAnalysisItem shouldHaveDeleteButton() {
+    elt.find(".js-analysis-actions").click();
     elt.find(".js-delete-analysis").shouldBe(visible);
     return this;
   }
 
   public ProjectAnalysisItem shouldNotHaveDeleteButton() {
+    elt.find(".js-analysis-actions").click();
     elt.find(".js-delete-analysis").shouldNotBe(visible);
     return this;
   }
 
   public void delete() {
+    elt.find(".js-analysis-actions").click();
     elt.find(".js-delete-analysis").click();
 
     SelenideElement modal = $(".modal");
@@ -59,7 +62,7 @@ public class ProjectAnalysisItem {
   }
 
   public ProjectAnalysisItem addCustomEvent(String name) {
-    elt.find(".js-create").click();
+    elt.find(".js-analysis-actions").click();
     elt.find(".js-add-event").click();
 
     SelenideElement modal = $(".modal");
@@ -67,30 +70,28 @@ public class ProjectAnalysisItem {
     modal.find("input").setValue(name);
     modal.find("button[type=\"submit\"]").click();
 
-    elt.find(".project-activity-event:last-child").shouldHave(text(name));
-
+    elt.find(".project-activity-event:first-child").shouldHave(text(name));
     return this;
   }
 
-  public ProjectAnalysisItem changeLastEvent(String newName) {
-    SelenideElement lastEvent = elt.find(".project-activity-event:last-child");
-    lastEvent.find(".js-change-event").click();
+  public ProjectAnalysisItem changeFirstEvent(String newName) {
+    SelenideElement firstEvent = elt.find(".project-activity-event:first-child");
+    firstEvent.find(".js-change-event").click();
 
     SelenideElement modal = $(".modal");
     modal.shouldBe(visible);
     modal.find("input").setValue(newName);
     modal.find("button[type=\"submit\"]").click();
 
-    lastEvent.shouldHave(text(newName));
-
+    firstEvent.shouldHave(text(newName));
     return this;
   }
 
-  public ProjectAnalysisItem deleteLastEvent() {
+  public ProjectAnalysisItem deleteFirstEvent() {
     int eventsCount = elt.findAll(".project-activity-event").size();
 
-    SelenideElement lastEvent = elt.find(".project-activity-event:last-child");
-    lastEvent.find(".js-delete-event").click();
+    SelenideElement firstEvent = elt.find(".project-activity-event:first-child");
+    firstEvent.find(".js-delete-event").click();
 
     SelenideElement modal = $(".modal");
     modal.shouldBe(visible);
index 5a10c83e607372e36d7fed2cd54e71c338e26135..d6514a930f0d570797a3589bc36b30562ec30785 100644 (file)
@@ -72,15 +72,17 @@ public class ProjectActivityPageTest {
     analyzeProject();
     openPage().getLastAnalysis()
       .addCustomEvent("foo")
-      .changeLastEvent("bar")
-      .deleteLastEvent();
+      .changeFirstEvent("bar")
+      .deleteFirstEvent();
   }
 
   @Test
   public void delete_analysis() {
     analyzeProject();
     analyzeProject();
-    openPage().getFirstAnalysis().delete();
+    ProjectActivityPage page = openPage();
+    page.getAnalyses().shouldHaveSize(2);
+    page.getFirstAnalysis().delete();
   }
 
   private ProjectActivityPage openPage() {