]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7674 Add Activity Stream interface (#1459)
authorStas Vilchik <vilchiks@gmail.com>
Mon, 19 Dec 2016 13:07:32 +0000 (14:07 +0100)
committerGitHub <noreply@github.com>
Mon, 19 Dec 2016 13:07:32 +0000 (14:07 +0100)
72 files changed:
it/it-tests/src/test/java/it/Category4Suite.java
it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java
it/it-tests/src/test/java/it/projectAdministration/ProjectHistoryPageTest.java [deleted file]
it/it-tests/src/test/java/it/projectEvent/EventTest.java
it/it-tests/src/test/java/it/projectEvent/ProjectActivityPageTest.java [new file with mode: 0644]
it/it-tests/src/test/java/it/qualityGate/QualityGateUiTest.java
it/it-tests/src/test/java/pageobjects/Navigation.java
it/it-tests/src/test/java/pageobjects/ProjectActivityPage.java [new file with mode: 0644]
it/it-tests/src/test/java/pageobjects/ProjectAnalysisItem.java [new file with mode: 0644]
it/it-tests/src/test/java/pageobjects/ProjectHistoryPage.java [deleted file]
it/it-tests/src/test/java/pageobjects/ProjectHistorySnapshotItem.java [deleted file]
it/it-tests/src/test/resources/projectEvent/EventTest/create_delete_standard_event.html [deleted file]
server/sonar-web/package.json
server/sonar-web/src/main/js/api/projectActivity.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/overview/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/Analysis.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/Event.js [deleted file]
server/sonar-web/src/main/js/apps/overview/events/EventsList.js [deleted file]
server/sonar-web/src/main/js/apps/overview/events/EventsListFilter.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta/Meta.js
server/sonar-web/src/main/js/apps/overview/styles.css
server/sonar-web/src/main/js/apps/projectActivity/actions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ChangeIcon.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/DeleteIcon.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/Event.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/Event.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/Events.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/routes.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/FormattedDate.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/analyses.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/duck.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/events.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/projectActivity/paging.js [new file with mode: 0644]
server/sonar-web/src/main/js/store/rootActions.js
server/sonar-web/src/main/js/store/rootReducer.js
server/sonar-web/src/main/js/store/utils/configureStore.js
server/sonar-web/src/main/less/components/modals.less
server/sonar-web/src/main/less/init/forms.less
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 4e6e08af9805e3307e59a307ec3e874679152d4c..db87ef57d3cf327f68f357154cc95b14a6f3857c 100644 (file)
@@ -30,6 +30,7 @@ import it.duplication.CrossProjectDuplicationsTest;
 import it.duplication.DuplicationsTest;
 import it.duplication.NewDuplicationsTest;
 import it.projectEvent.EventTest;
+import it.projectEvent.ProjectActivityPageTest;
 import it.projectSearch.SearchProjectsTest;
 import it.qualityProfile.QualityProfilesPageTest;
 import it.serverSystem.HttpHeadersTest;
@@ -80,6 +81,7 @@ import static util.ItUtils.xooPlugin;
   PurgeTest.class,
   // project event
   EventTest.class,
+  ProjectActivityPageTest.class,
   // project search
   SearchProjectsTest.class,
   // http
index 044e342a07c5a8d8394f9d9708e9e11671001fa5..e9aa4ba0f990fe42696aedc23873bb92b5d42df9 100644 (file)
@@ -128,7 +128,7 @@ public class ProjectAdministrationTest {
 
   // SONAR-4203
   @Test
-  @Ignore("history page is not available yet")
+  @Ignore("refactor with wsClient")
   public void delete_version_of_multimodule_project() {
     GregorianCalendar today = new GregorianCalendar();
     SonarScanner build = SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"))
diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectHistoryPageTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectHistoryPageTest.java
deleted file mode 100644 (file)
index f7e2afd..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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.
- */
-package it.projectAdministration;
-
-import com.sonar.orchestrator.Orchestrator;
-import com.sonar.orchestrator.build.SonarScanner;
-import it.Category1Suite;
-import java.util.List;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
-import pageobjects.Navigation;
-import pageobjects.ProjectHistoryPage;
-import pageobjects.ProjectHistorySnapshotItem;
-
-import static com.codeborne.selenide.Condition.exist;
-import static com.codeborne.selenide.Condition.text;
-import static com.codeborne.selenide.Selenide.confirm;
-import static util.ItUtils.projectDir;
-
-public class ProjectHistoryPageTest {
-
-  @ClassRule
-  public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR;
-
-  @Rule
-  public Navigation nav = Navigation.get(ORCHESTRATOR);
-
-  @Before
-  public void setUp() {
-    ORCHESTRATOR.resetData();
-    analyzeProject("shared/xoo-history-v1", "2014-10-19");
-    analyzeProject("shared/xoo-history-v2", "2014-11-13");
-  }
-
-  @Test
-  public void should_list_snapshots() {
-    ProjectHistoryPage page = openPage();
-
-    page.getSnapshots().shouldHaveSize(2);
-
-    List<ProjectHistorySnapshotItem> snapshots = page.getSnapshotsAsItems();
-
-    snapshots.get(0).getVersionText().shouldBe(text("1.0-SNAPSHOT"));
-    snapshots.get(0).getDeleteButton().shouldNot(exist);
-
-    snapshots.get(1).getVersionText().shouldBe(text("0.9-SNAPSHOT"));
-    snapshots.get(1).getDeleteButton().should(exist);
-  }
-
-  @Test
-  public void should_delete_snapshot() {
-    ProjectHistoryPage page = openPage();
-
-    page.getSnapshots().shouldHaveSize(2);
-
-    page.getSnapshotsAsItems().get(1).clickDelete();
-    confirm();
-
-    page.checkAlertDisplayed();
-    page.getSnapshots().shouldHaveSize(1);
-  }
-
-  private ProjectHistoryPage openPage() {
-    nav.logIn().submitCredentials("admin", "admin");
-    return nav.openProjectHistory("sample");
-  }
-
-  private static void analyzeProject(String path, String date) {
-    ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir(path))
-      .setProperties("sonar.projectDate", date));
-  }
-}
index 062e26669668967a4641df9726698beb93f6d732..749e27a1ec6e3c8f545eac3cce34e6e32c55c3a5 100644 (file)
@@ -37,9 +37,8 @@ import util.ItUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static util.ItUtils.projectDir;
-import static util.selenium.Selenese.runSelenese;
 
-@Ignore("history page is not available yet")
+@Ignore("refactor using wsClient")
 public class EventTest {
 
   @ClassRule
@@ -50,7 +49,6 @@ public class EventTest {
     orchestrator.resetData();
   }
 
-  @Ignore("UUID column of Events is not handled with Ruby pages and WS")
   @Test
   public void old_ws_events_does_not_allow_creating_events_on_modules() {
     SonarScanner sampleProject = SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"));
@@ -72,14 +70,6 @@ public class EventTest {
       .setParam("category", "Foo");
   }
 
-  @Ignore("UUID column of Events is not handled with Ruby pages and WS")
-  @Test
-  public void delete_standard_event() {
-    executeAnalysis();
-
-    runSelenese(orchestrator, "/projectEvent/EventTest/create_delete_standard_event.html");
-  }
-
   /**
    * SONAR-3308
    */
diff --git a/it/it-tests/src/test/java/it/projectEvent/ProjectActivityPageTest.java b/it/it-tests/src/test/java/it/projectEvent/ProjectActivityPageTest.java
new file mode 100644 (file)
index 0000000..cc2d706
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+package it.projectEvent;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import it.Category1Suite;
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import pageobjects.Navigation;
+import pageobjects.ProjectActivityPage;
+import pageobjects.ProjectAnalysisItem;
+
+import static util.ItUtils.projectDir;
+
+public class ProjectActivityPageTest {
+
+  @ClassRule
+  public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR;
+
+  @Rule
+  public Navigation nav = Navigation.get(ORCHESTRATOR);
+
+  @Before
+  public void setUp() throws Exception {
+    ORCHESTRATOR.resetData();
+  }
+
+  @Test
+  public void should_list_snapshots() {
+    analyzeProject("shared/xoo-history-v1", "2014-10-19");
+    analyzeProject("shared/xoo-history-v2", "2014-11-13");
+
+    ProjectActivityPage page = openPage();
+    page.getAnalyses().shouldHaveSize(2);
+
+    List<ProjectAnalysisItem> analyses = page.getAnalysesAsItems();
+    analyses.get(0)
+      .shouldHaveEventWithText("1.0-SNAPSHOT")
+      .shouldNotHaveDeleteButton();
+
+    analyses.get(1)
+      .shouldHaveEventWithText("0.9-SNAPSHOT")
+      .shouldHaveDeleteButton();
+  }
+
+  @Test
+  public void add_change_delete_custom_event() {
+    analyzeProject();
+    openPage().getLastAnalysis()
+      .addCustomEvent("foo")
+      .changeLastEvent("bar")
+      .deleteLastEvent();
+  }
+
+  @Test
+  public void delete_analysis() {
+    analyzeProject();
+    analyzeProject();
+    openPage().getFirstAnalysis().delete();
+  }
+
+  private ProjectActivityPage openPage() {
+    nav.logIn().submitCredentials("admin", "admin");
+    return nav.openProjectActivity("sample");
+  }
+
+  private static void analyzeProject() {
+    ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample")));
+  }
+
+  private static void analyzeProject(String path, String date) {
+    ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir(path)).setProperties("sonar.projectDate", date));
+  }
+}
index ef5fdf25a82445f4d9cb02715c123e4ab4d93c25..5ee509687fbff6ffd23de52a868dd95f7e62758f 100644 (file)
@@ -27,13 +27,14 @@ import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.sonar.wsclient.qualitygate.NewCondition;
 import org.sonar.wsclient.qualitygate.QualityGate;
 import org.sonar.wsclient.qualitygate.QualityGateClient;
 import org.sonar.wsclient.qualitygate.QualityGateCondition;
 import org.sonar.wsclient.qualitygate.UpdateCondition;
+import pageobjects.Navigation;
+import pageobjects.ProjectActivityPage;
 import util.ItUtils;
 
 import static util.ItUtils.projectDir;
@@ -70,7 +71,6 @@ public class QualityGateUiTest {
    * SONAR-3326
    */
   @Test
-  @Ignore("history page is not available yet")
   public void display_alerts_correctly_in_history_page() {
     QualityGateClient qgClient = qgClient();
     QualityGate qGate = qgClient.create("AlertsForHistory");
@@ -83,7 +83,10 @@ public class QualityGateUiTest {
     qgClient.updateCondition(UpdateCondition.create(lowThresholds.id()).metricKey("lines").operator("GT").warningThreshold("5000").errorThreshold("5000"));
     scanSampleWithDate("2012-01-02");
 
-    runSelenese(orchestrator, "/qualityGate/QualityGateUiTest/should-display-alerts-correctly-history-page.html");
+    ProjectActivityPage page = Navigation.get(orchestrator).openProjectActivity("sample");
+    page
+      .assertFirstAnalysisOfTheDayHasText("2012-01-02", "Green (was Orange)")
+      .assertFirstAnalysisOfTheDayHasText("2012-01-01", "Orange");
 
     qgClient.unsetDefault();
     qgClient.destroy(qGate.id());
index 840f4b2f65831b2ce8a230eb7fd636550426ff19..e9c23d63f14b913d3a70ea9e34b8c458a13d1c07 100644 (file)
@@ -79,18 +79,18 @@ public class Navigation extends ExternalResource {
     return open(url, ProjectQualityGatePage.class);
   }
 
-  public ProjectHistoryPage openProjectHistory(String projectKey) {
-    // TODO encode projectKey
-    String url = "/project/history?id=" + projectKey;
-    return open(url, ProjectHistoryPage.class);
-  }
-
   public ProjectKeyPage openProjectKey(String projectKey) {
     // TODO encode projectKey
     String url = "/project/key?id=" + projectKey;
     return open(url, ProjectKeyPage.class);
   }
 
+  public ProjectActivityPage openProjectActivity(String projectKey) {
+    // TODO encode projectKey
+    String url = "/project/activity?id=" + projectKey;
+    return open(url, ProjectActivityPage.class);
+  }
+
   public BackgroundTasksPage openBackgroundTasksPage() {
     return open("/background_tasks", BackgroundTasksPage.class);
   }
diff --git a/it/it-tests/src/test/java/pageobjects/ProjectActivityPage.java b/it/it-tests/src/test/java/pageobjects/ProjectActivityPage.java
new file mode 100644 (file)
index 0000000..8acf892
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+package pageobjects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.ElementsCollection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.codeborne.selenide.Condition.hasText;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class ProjectActivityPage {
+
+  public ProjectActivityPage() {
+    $("#project-activity").should(Condition.exist);
+  }
+
+  public ElementsCollection getAnalyses() {
+    return $$(".project-activity-analysis");
+  }
+
+  public List<ProjectAnalysisItem> getAnalysesAsItems() {
+    return getAnalyses()
+      .stream()
+      .map(ProjectAnalysisItem::new)
+      .collect(Collectors.toList());
+  }
+
+  public ProjectAnalysisItem getLastAnalysis() {
+    return new ProjectAnalysisItem($(".project-activity-analysis"));
+  }
+
+  public ProjectAnalysisItem getFirstAnalysis() {
+    return new ProjectAnalysisItem($$(".project-activity-analysis").last());
+  }
+
+  public ProjectActivityPage assertFirstAnalysisOfTheDayHasText(String day, String text) {
+    $("#project-activity")
+      .find(".project-activity-day[data-day=\"" + day + "\"]")
+      .find(".project-activity-analysis")
+      .should(hasText(text));
+    return this;
+  }
+}
diff --git a/it/it-tests/src/test/java/pageobjects/ProjectAnalysisItem.java b/it/it-tests/src/test/java/pageobjects/ProjectAnalysisItem.java
new file mode 100644 (file)
index 0000000..84de55e
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+package pageobjects;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+
+public class ProjectAnalysisItem {
+
+  private final SelenideElement elt;
+
+  public ProjectAnalysisItem(SelenideElement elt) {
+    this.elt = elt;
+  }
+
+  public ProjectAnalysisItem shouldHaveEventWithText(String text) {
+    elt.find(".project-activity-events").shouldHave(Condition.text(text));
+    return this;
+  }
+
+  public ProjectAnalysisItem shouldHaveDeleteButton() {
+    elt.find(".js-delete-analysis").shouldBe(visible);
+    return this;
+  }
+
+  public ProjectAnalysisItem shouldNotHaveDeleteButton() {
+    elt.find(".js-delete-analysis").shouldNotBe(visible);
+    return this;
+  }
+
+  public void delete() {
+    elt.find(".js-delete-analysis").click();
+
+    SelenideElement modal = $(".modal");
+    modal.shouldBe(visible);
+    modal.find("button[type=\"submit\"]").click();
+
+    elt.shouldNotBe(visible);
+  }
+
+  public ProjectAnalysisItem addCustomEvent(String name) {
+    elt.find(".js-create").click();
+    elt.find(".js-add-event").click();
+
+    SelenideElement modal = $(".modal");
+    modal.shouldBe(visible);
+    modal.find("input").setValue(name);
+    modal.find("button[type=\"submit\"]").click();
+
+    elt.find(".project-activity-event:last-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();
+
+    SelenideElement modal = $(".modal");
+    modal.shouldBe(visible);
+    modal.find("input").setValue(newName);
+    modal.find("button[type=\"submit\"]").click();
+
+    lastEvent.shouldHave(text(newName));
+
+    return this;
+  }
+
+  public ProjectAnalysisItem deleteLastEvent() {
+    int eventsCount = elt.findAll(".project-activity-event").size();
+
+    SelenideElement lastEvent = elt.find(".project-activity-event:last-child");
+    lastEvent.find(".js-delete-event").click();
+
+    SelenideElement modal = $(".modal");
+    modal.shouldBe(visible);
+    modal.find("button[type=\"submit\"]").click();
+
+    elt.findAll(".project-activity-event").shouldHaveSize(eventsCount - 1);
+
+    return this;
+  }
+}
diff --git a/it/it-tests/src/test/java/pageobjects/ProjectHistoryPage.java b/it/it-tests/src/test/java/pageobjects/ProjectHistoryPage.java
deleted file mode 100644 (file)
index 83b0f5c..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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.
- */
-package pageobjects;
-
-import com.codeborne.selenide.ElementsCollection;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import static com.codeborne.selenide.Condition.exist;
-import static com.codeborne.selenide.Selenide.$;
-import static com.codeborne.selenide.Selenide.$$;
-
-public class ProjectHistoryPage {
-
-  public ProjectHistoryPage() {
-    $("#project-history").should(exist);
-  }
-
-  public ElementsCollection getSnapshots() {
-    return $$("tr.snapshot");
-  }
-
-  public List<ProjectHistorySnapshotItem> getSnapshotsAsItems() {
-    return getSnapshots()
-      .stream()
-      .map(ProjectHistorySnapshotItem::new)
-      .collect(Collectors.toList());
-  }
-
-  public void checkAlertDisplayed() {
-    $("#info:not(.hidden)").should(exist);
-  }
-}
diff --git a/it/it-tests/src/test/java/pageobjects/ProjectHistorySnapshotItem.java b/it/it-tests/src/test/java/pageobjects/ProjectHistorySnapshotItem.java
deleted file mode 100644 (file)
index d265e98..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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.
- */
-package pageobjects;
-
-import com.codeborne.selenide.SelenideElement;
-import org.openqa.selenium.NoSuchElementException;
-
-public class ProjectHistorySnapshotItem {
-
-  private final SelenideElement elt;
-
-  public ProjectHistorySnapshotItem(SelenideElement elt) {
-    this.elt = elt;
-  }
-
-  public SelenideElement getVersionText() {
-    return elt.$("td:nth-child(5) table td:nth-child(1)");
-  }
-
-  public SelenideElement getType() {
-    try {
-      return elt.$(".js-type");
-    } catch (NoSuchElementException e) {
-      return null;
-    }
-  }
-
-  public SelenideElement getUrl() {
-    return elt.$(".js-url");
-  }
-
-  public SelenideElement getDeleteButton() {
-    return elt.$("td:nth-child(9) input[type=\"submit\"]");
-  }
-
-  public void clickDelete() {
-    getDeleteButton().click();
-  }
-}
diff --git a/it/it-tests/src/test/resources/projectEvent/EventTest/create_delete_standard_event.html b/it/it-tests/src/test/resources/projectEvent/EventTest/create_delete_standard_event.html
deleted file mode 100644 (file)
index ae697da..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <title>create_delete_standard_event</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-  <thead>
-  <tr>
-    <td rowspan="1" colspan="3">create_delete_standard_event</td>
-  </tr>
-  </thead>
-  <tbody>
-  <tr>
-       <td>open</td>
-       <td>/sessions/logout</td>
-       <td></td>
-</tr>
-<tr>
-       <td>open</td>
-       <td>/sessions/login</td>
-       <td></td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>login</td>
-       <td>admin</td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>password</td>
-       <td>admin</td>
-</tr>
-<tr>
-       <td>clickAndWait</td>
-       <td>commit</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>css=.js-user-authenticated</td>
-       <td></td>
-</tr>
-<tr>
-       <td>open</td>
-       <td>/project/history?id=sample</td>
-       <td></td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>link=Create</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>create_event_name_0</td>
-       <td></td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>create_event_name_0</td>
-       <td>EventToBeDeleted</td>
-</tr>
-<tr>
-       <td>clickAndWait</td>
-       <td>create_save_event_0</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>infomsg</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForText</td>
-       <td>infomsg</td>
-       <td>Event 'EventToBeDeleted' was created.</td>
-</tr>
-<tr>
-       <td>assertElementPresent</td>
-       <td>//td[text()='EventToBeDeleted']</td>
-       <td></td>
-</tr>
-<tr>
-       <td>clickAndWait</td>
-       <td>link=Remove</td>
-       <td></td>
-</tr>
-<tr>
-       <td>assertConfirmation</td>
-       <td>Are you sure you want to remove 'EventToBeDeleted' from this snapshot?</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>infomsg</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForText</td>
-       <td>infomsg</td>
-       <td>Event 'EventToBeDeleted' was deleted.</td>
-</tr>
-<tr>
-       <td>assertElementNotPresent</td>
-       <td>//td[text()='EventToBeDeleted']</td>
-       <td></td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>
index 36c49704e411e3904250acec6ab32e214e22f3f3..7f343115d4b6075ed5b5c3d9288199058f493f77 100644 (file)
@@ -66,6 +66,7 @@
     "react-dev-utils": "0.2.1",
     "react-dom": "15.3.2",
     "react-helmet": "3.1.0",
+    "react-modal": "^1.6.4",
     "react-redux": "4.4.1",
     "react-router": "2.8.1",
     "react-router-redux": "4.0.2",
@@ -93,7 +94,7 @@
     "test": "node scripts/test.js",
     "coverage": "npm test -- --coverage",
     "lint": "eslint src/main/js",
-    "typecheck": "flow check src/main/js"
+    "typecheck": "flow src/main/js"
   },
   "engines": {
     "node": ">=4"
diff --git a/server/sonar-web/src/main/js/api/projectActivity.js b/server/sonar-web/src/main/js/api/projectActivity.js
new file mode 100644 (file)
index 0000000..9131cb9
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import { getJSON, postJSON, post } from '../helpers/request';
+
+type GetProjectActivityResponse = {
+  analyses: Array<Object>,
+  paging: {
+    total: number,
+    pageIndex: number,
+    pageSize: number
+  }
+};
+
+type GetProjectActivityOptions = {
+  category?: ?string,
+  pageIndex?: ?number,
+  pageSize?: ?number
+};
+
+export const getProjectActivity = (
+    project: string,
+    options?: GetProjectActivityOptions
+): Promise<GetProjectActivityResponse> => {
+  const data: Object = { project };
+  if (options) {
+    if (options.category) {
+      data.category = options.category;
+    }
+    if (options.pageIndex) {
+      data.p = options.pageIndex;
+    }
+    if (options.pageSize) {
+      data.ps = options.pageSize;
+    }
+  }
+
+  return getJSON('/api/project_analyses/search', data);
+};
+
+type CreateEventResponse = {
+  analysis: string,
+  key: string,
+  name: string,
+  category: string,
+  description?: string
+};
+
+export const createEvent = (
+    analysis: string,
+    name: string,
+    category?: string,
+    description?: string
+): Promise<CreateEventResponse> => {
+  const data: Object = { analysis, name };
+  if (category) {
+    data.category = category;
+  }
+  if (description) {
+    data.description = description;
+  }
+  return postJSON('/api/project_analyses/create_event', data).then(r => r.event);
+};
+
+export const deleteEvent = (event: string): Promise<*> => (
+    post('/api/project_analyses/delete_event', { event })
+);
+
+export const changeEvent = (event: string, name: ?string, description: ?string): Promise<CreateEventResponse> => {
+  const data: Object = { event };
+  if (name) {
+    data.name = name;
+  }
+  if (description) {
+    data.description = description;
+  }
+  return postJSON('/api/project_analyses/update_event', data).then(r => r.event);
+};
+
+export const deleteAnalysis = (analysis: string): Promise<*> => (
+    post('/api/project_analyses/delete', { analysis })
+);
index c348e186615c39b83798184be64057ede2fa3502..205e203b3d57654dfd86b286c498e3fd88acc1c0 100644 (file)
@@ -95,6 +95,17 @@ export default class ComponentNavMenu extends React.Component {
     );
   }
 
+  renderActivityLink () {
+    return (
+        <li>
+          <Link to={{ pathname: '/project/activity', query: { id: this.props.component.key } }}
+                activeClassName="active">
+            {translate('project_activity.page')}
+          </Link>
+        </li>
+    );
+  }
+
   renderComponentIssuesLink () {
     return (
         <li>
@@ -138,7 +149,6 @@ export default class ComponentNavMenu extends React.Component {
             {this.renderCustomMeasuresLink()}
             {this.renderLinksLink()}
             {this.renderPermissionsLink()}
-            {this.renderHistoryLink()}
             {this.renderBackgroundTasksLink()}
             {this.renderUpdateKeyLink()}
             {this.renderExtensions()}
@@ -238,21 +248,6 @@ export default class ComponentNavMenu extends React.Component {
     );
   }
 
-  renderHistoryLink () {
-    if (!this.props.conf.showHistory) {
-      return null;
-    }
-    const url = `/project/history?id=${encodeURIComponent(this.props.component.key)}`;
-    // return this.renderLink(url, translate('project_history.page'), '/project/history');
-    return (
-        <li key={url}>
-          <span className="text-muted" style={{ cursor: 'not-allowed', textDecoration: 'line-through' }}>
-            {translate('project_history.page')}
-          </span>
-        </li>
-    );
-  }
-
   renderBackgroundTasksLink () {
     if (!this.props.conf.showBackgroundTasks) {
       return null;
@@ -336,6 +331,7 @@ export default class ComponentNavMenu extends React.Component {
           {this.renderComponentIssuesLink()}
           {this.renderComponentMeasuresLink()}
           {this.renderCodeLink()}
+          {this.renderActivityLink()}
           {this.renderTools()}
           {this.renderAdministration()}
         </ul>
index 6d30a4462904ef6f776bbd52d084c63ca81a42cf..9edb27a8ff2aafd2efaf75d51570215d2645175f 100644 (file)
@@ -98,7 +98,7 @@ export default Marionette.LayoutView.extend({
   },
 
   events: {
-    'submit': 'onSubmit',
+    'submit': 'handleSubmit',
     'keydown .js-search-input': 'onKeyDown',
     'keyup .js-search-input': 'onKeyUp'
   },
index e92ba5d89965cbc605dfcae0a98a22a6fbffdb39..2c04125c885baa47d0473609f208f44fd7d7f7ec 100644 (file)
@@ -45,6 +45,7 @@ import issuesRoutes from '../../apps/issues/routes';
 import metricsRoutes from '../../apps/metrics/routes';
 import overviewRoutes from '../../apps/overview/routes';
 import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
+import projectActivityRoutes from '../../apps/projectActivity/routes';
 import projectAdminRoutes from '../../apps/project-admin/routes';
 import projectsRoutes from '../../apps/projects/routes';
 import projectsAdminRoutes from '../../apps/projects-admin/routes';
@@ -109,6 +110,7 @@ const startReactApp = () => {
                     <Route path="custom_measures">{customMeasuresRoutes}</Route>
                     <Route path="dashboard">{overviewRoutes}</Route>
                     <Route path="project">
+                      <Route path="activity">{projectActivityRoutes}</Route>
                       <Route path="background_tasks">{backgroundTasksRoutes}</Route>
                       <Route path="settings">{settingsRoutes}</Route>
                       {projectAdminRoutes}
diff --git a/server/sonar-web/src/main/js/apps/overview/actions.js b/server/sonar-web/src/main/js/apps/overview/actions.js
new file mode 100644 (file)
index 0000000..9913609
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import * as api from '../../api/projectActivity';
+import { receiveProjectActivity } from '../../store/projectActivity/duck';
+import { onFail } from '../../store/rootActions';
+
+const PAGE_SIZE = 5;
+
+export const fetchRecentProjectActivity = (project: string) => (dispatch: Function) => (
+    api.getProjectActivity(project, { pageSize: PAGE_SIZE }).then(
+        ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)),
+        onFail(dispatch)
+    )
+);
diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
new file mode 100644 (file)
index 0000000..842a968
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { Link } from 'react-router';
+import { connect } from 'react-redux';
+import Analysis from './Analysis';
+import { translate } from '../../../helpers/l10n';
+import { fetchRecentProjectActivity } from '../actions';
+import { getProjectActivity } from '../../../store/rootReducer';
+import { getAnalyses } from '../../../store/projectActivity/duck';
+
+type Props = {
+  analyses?: Array<*>,
+  project: string;
+  fetchRecentProjectActivity: (project: string) => Promise<*>;
+}
+
+class AnalysesList extends React.Component {
+  mounted: boolean;
+  props: Props;
+
+  state = {
+    loading: true
+  };
+
+  componentDidMount () {
+    this.mounted = true;
+    this.fetchData();
+  }
+
+  componentDidUpdate (prevProps: Props) {
+    if (prevProps.project !== this.props.project) {
+      this.fetchData();
+    }
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  fetchData () {
+    this.setState({ loading: true });
+    this.props.fetchRecentProjectActivity(this.props.project).then(() => {
+      if (this.mounted) {
+        this.setState({ loading: false });
+      }
+    });
+  }
+
+  renderList (analyses) {
+    if (!analyses.length) {
+      return (
+          <p className="spacer-top note">
+            {translate('no_results')}
+          </p>
+      );
+    }
+
+    return (
+        <ul className="spacer-top">
+          {analyses.map(analysis => (
+              <Analysis key={analysis.key} analysis={analysis}/>
+          ))}
+        </ul>
+    );
+  }
+
+  render () {
+    const { analyses } = this.props;
+    const { loading } = this.state;
+
+    if (loading || !analyses) {
+      return null;
+    }
+
+    return (
+        <div className="overview-meta-card">
+          <h4 className="overview-meta-header">
+            {translate('project_activity.page')}
+          </h4>
+
+          {this.renderList(analyses)}
+
+          <div className="spacer-top small">
+            <Link to={{ pathname: '/project/activity', query: { id: this.props.project } }}>
+              {translate('show_more')}
+            </Link>
+          </div>
+        </div>
+    );
+  }
+}
+
+const mapStateToProps = (state, ownProps: Props) => ({
+  analyses: getAnalyses(getProjectActivity(state), ownProps.project)
+});
+
+const mapDispatchToProps = { fetchRecentProjectActivity };
+
+export default connect(mapStateToProps, mapDispatchToProps)(AnalysesList);
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js
new file mode 100644 (file)
index 0000000..a168b08
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 React from 'react';
+import Events from '../../projectActivity/components/Events';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
+import { translate } from '../../../helpers/l10n';
+import type { Analysis as AnalysisType } from '../../../store/projectActivity/duck';
+
+export default class Analysis extends React.Component {
+  props: {
+    analysis: AnalysisType
+  };
+
+  render () {
+    const { analysis } = this.props;
+
+    return (
+        <TooltipsContainer>
+          <li className="overview-analysis">
+            <div className="small little-spacer-bottom">
+              <strong>
+                <FormattedDate date={analysis.date} format="LL"/>
+              </strong>
+            </div>
+
+            {analysis.events.length > 0 ? (
+                <Events events={analysis.events} canAdmin={false}/>
+            ) : (
+                <span className="note">{translate('project_activity.project_analyzed')}</span>
+            )}
+          </li>
+        </TooltipsContainer>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.js b/server/sonar-web/src/main/js/apps/overview/events/Event.js
deleted file mode 100644 (file)
index acef6b7..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 React from 'react';
-import moment from 'moment';
-
-import { EventType } from '../propTypes';
-import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
-import { translate } from '../../../helpers/l10n';
-
-const Event = ({ event }) => {
-  return (
-      <TooltipsContainer>
-        <li className="spacer-top">
-          <p>
-            <strong className="js-event-type">
-              {translate('event.category', event.type)}
-            </strong>
-            {': '}
-            <span className="js-event-name">{event.name}</span>
-            {event.text && (
-                <i
-                    className="spacer-left icon-help"
-                    data-toggle="tooltip"
-                    title={event.text}/>
-            )}
-          </p>
-          <p className="note little-spacer-top js-event-date">
-            {moment(event.date).format('LL')}
-          </p>
-        </li>
-      </TooltipsContainer>
-  );
-};
-
-Event.propTypes = {
-  event: EventType.isRequired
-};
-
-export default Event;
diff --git a/server/sonar-web/src/main/js/apps/overview/events/EventsList.js b/server/sonar-web/src/main/js/apps/overview/events/EventsList.js
deleted file mode 100644 (file)
index 3b35a24..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 moment from 'moment';
-import React from 'react';
-import shallowCompare from 'react-addons-shallow-compare';
-
-import Event from './Event';
-import EventsListFilter from './EventsListFilter';
-import { getEvents } from '../../../api/events';
-import { translate } from '../../../helpers/l10n';
-
-const LIMIT = 5;
-
-export default class EventsList extends React.Component {
-  state = {
-    events: [],
-    limited: true,
-    filter: 'All'
-  };
-
-  componentDidMount () {
-    this.mounted = true;
-    this.fetchEvents();
-  }
-
-  shouldComponentUpdate (nextProps, nextState) {
-    return shallowCompare(this, nextProps, nextState);
-  }
-
-  componentDidUpdate (nextProps) {
-    if (nextProps.component !== this.props.component) {
-      this.fetchEvents();
-    }
-  }
-
-  componentWillUnmount () {
-    this.mounted = false;
-  }
-
-  fetchEvents () {
-    getEvents(this.props.component.key).then(events => {
-      if (this.mounted) {
-        const nextEvents = events.map(event => {
-          return {
-            id: event.id,
-            date: moment(event.dt).toDate(),
-            type: event.c,
-            name: event.n,
-            text: event.ds
-          };
-        });
-
-        this.setState({ events: nextEvents });
-      }
-    });
-  }
-
-  limitEvents (events) {
-    return this.state.limited ? events.slice(0, LIMIT) : events;
-  }
-
-  filterEvents (events) {
-    if (this.state.filter === 'All') {
-      return events;
-    } else {
-      return events.filter(event => event.type === this.state.filter);
-    }
-  }
-
-  handleClick (e) {
-    e.preventDefault();
-    this.setState({ limited: !this.state.limited });
-  }
-
-  handleFilter (filter) {
-    this.setState({ filter });
-  }
-
-  renderMoreLink () {
-    const text = this.state.limited ?
-        translate('widget.events.show_all') :
-        translate('hide');
-
-    return (
-        <p className="spacer-top note">
-          <a onClick={this.handleClick.bind(this)} href="#">{text}</a>
-        </p>
-    );
-  }
-
-  renderList (events) {
-    if (events.length) {
-      return (
-          <ul>
-            {events.map(event => (
-                <Event key={event.id} event={event}/>
-            ))}
-          </ul>
-      );
-    } else {
-      return (
-          <p className="spacer-top note">
-            {translate('no_results')}
-          </p>
-      );
-    }
-  }
-
-  render () {
-    const filteredEvents = this.filterEvents(this.state.events);
-    const events = this.limitEvents(filteredEvents);
-
-    return (
-        <div className="overview-meta-card">
-          <div className="clearfix">
-            <h4 className="pull-left overview-meta-header">
-              {translate('widget.events.name')}
-            </h4>
-            <div className="pull-right">
-              <EventsListFilter
-                  currentFilter={this.state.filter}
-                  onFilter={this.handleFilter.bind(this)}/>
-            </div>
-          </div>
-
-          {this.renderList(events)}
-
-          {filteredEvents.length > LIMIT && this.renderMoreLink()}
-        </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/EventsListFilter.js b/server/sonar-web/src/main/js/apps/overview/events/EventsListFilter.js
deleted file mode 100644 (file)
index cfd142f..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 React from 'react';
-import Select from 'react-select';
-import { translate } from '../../../helpers/l10n';
-
-const TYPES = ['All', 'Version', 'Alert', 'Profile', 'Other'];
-
-const EventsListFilter = ({ currentFilter, onFilter }) => {
-  const handleChange = selected => onFilter(selected.value);
-
-  const options = TYPES.map(type => {
-    return {
-      value: type,
-      label: translate('event.category', type)
-    };
-  });
-
-  return (
-      <Select
-          value={currentFilter}
-          options={options}
-          clearable={false}
-          searchable={false}
-          onChange={handleChange}
-          style={{ width: '125px' }}/>
-  );
-};
-
-EventsListFilter.propTypes = {
-  onFilter: React.PropTypes.func.isRequired,
-  currentFilter: React.PropTypes.string.isRequired
-};
-
-export default EventsListFilter;
index c034da4452b66fd21973dc87a3b09ff2f402e69f..26be7ed8ea291f7f04229b1cee5f4426cb3878ab 100644 (file)
@@ -23,7 +23,7 @@ import MetaKey from './MetaKey';
 import MetaLinks from './MetaLinks';
 import MetaQualityGate from './MetaQualityGate';
 import MetaQualityProfiles from './MetaQualityProfiles';
-import EventsList from './../events/EventsList';
+import AnalysesList from '../events/AnalysesList';
 import MetaSize from './MetaSize';
 
 const Meta = ({ component, measures }) => {
@@ -40,7 +40,7 @@ const Meta = ({ component, measures }) => {
   const shouldShowQualityProfiles = !isView && !isDeveloper && hasQualityProfiles;
   const shouldShowQualityGate = !isView && !isDeveloper && hasQualityGate;
 
-  const showShowEvents = isProject || isView || isDeveloper;
+  const showShowAnalyses = isProject || isView || isDeveloper;
 
   return (
       <div className="overview-meta">
@@ -64,8 +64,8 @@ const Meta = ({ component, measures }) => {
 
         <MetaKey component={component}/>
 
-        {showShowEvents && (
-            <EventsList component={component}/>
+        {showShowAnalyses && (
+            <AnalysesList project={component.key}/>
         )}
       </div>
   );
index cb9c344b814ae0be68f8a17324369119fdba4f05..088b19ca7e284f6feb7623f093f64e9ffbd0156a 100644 (file)
   box-sizing: border-box;
 }
 
+.overview-analysis {
+
+}
+
+.overview-analysis + .overview-analysis {
+  margin-top: 8px;
+  padding-top: 8px;
+  border-top: 1px solid #e6e6e6;
+}
+
 /*
  * Other
  */
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/actions.js b/server/sonar-web/src/main/js/apps/projectActivity/actions.js
new file mode 100644 (file)
index 0000000..83115bd
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import * as api from '../../api/projectActivity';
+import {
+  receiveProjectActivity,
+  addEvent,
+  deleteEvent as deleteEventAction,
+  changeEvent as changeEventAction,
+  deleteAnalysis as deleteAnalysisAction,
+  getPaging
+} from '../../store/projectActivity/duck';
+import { onFail } from '../../store/rootActions';
+import { getProjectActivity } from '../../store/rootReducer';
+
+const rejectOnFail = (dispatch: Function) => (error: any) => {
+  onFail(dispatch)(error);
+  return Promise.reject();
+};
+
+export const fetchProjectActivity = (project: string, filter: ?string) => (dispatch: Function): void => {
+  api.getProjectActivity(project, { category: filter }).then(
+      ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)),
+      onFail(dispatch)
+  );
+};
+
+export const fetchMoreProjectActivity = (project: string, filter: ?string) =>
+    (dispatch: Function, getState: Function): void => {
+      const projectActivity = getProjectActivity(getState());
+      const { pageIndex } = getPaging(projectActivity, project);
+
+      api.getProjectActivity(project, { category: filter, pageIndex: pageIndex + 1 }).then(
+          ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)),
+          onFail(dispatch)
+      );
+    };
+
+export const addCustomEvent = (analysis: string, name: string, category?: string) =>
+    (dispatch: Function): Promise<*> => {
+      return api.createEvent(analysis, name, category).then(
+          ({ analysis, ...event }) => dispatch(addEvent(analysis, event)),
+          rejectOnFail(dispatch)
+      );
+    };
+
+export const deleteEvent = (analysis: string, event: string) => (dispatch: Function): Promise<*> => {
+  return api.deleteEvent(event).then(
+      () => dispatch(deleteEventAction(analysis, event)),
+      rejectOnFail(dispatch)
+  );
+};
+
+export const addVersion = (analysis: string, version: string) => (dispatch: Function): Promise<*> => {
+  return dispatch(addCustomEvent(analysis, version, 'VERSION'));
+};
+
+export const changeEvent = (event: string, name: string) => (dispatch: Function): Promise<*> => {
+  return api.changeEvent(event, name).then(
+      () => dispatch(changeEventAction(event, { name })),
+      rejectOnFail(dispatch)
+  );
+};
+
+export const deleteAnalysis = (project: string, analysis: string) => (dispatch: Function): Promise<*> => {
+  return api.deleteAnalysis(analysis).then(
+      () => dispatch(deleteAnalysisAction(project, analysis)),
+      rejectOnFail(dispatch)
+  );
+};
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ChangeIcon.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ChangeIcon.js
new file mode 100644 (file)
index 0000000..c9a0862
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+
+export default class ChangeIcon extends React.Component {
+  render () {
+    /* eslint-disable max-len */
+    return (
+        <svg width="12" height="12" viewBox="0 0 14 14">
+          <path fill="#236a97"
+                d="M3.35 12.82l.85-.84L2.02 9.8l-.84.85v.98h1.2v1.2h.97zM8.2 4.24c0-.13-.08-.2-.22-.2-.06 0-.1.02-.15.06l-5 5c-.05.05-.08.1-.08.17 0 .13.07.2.2.2.07 0 .12-.02.16-.06l5.02-5c.05-.04.07-.1.07-.16zm-.5-1.77l3.83 3.84-7.7 7.7H0v-3.84l7.7-7.7zm6.3.88c0 .33-.1.6-.34.84L12.12 5.7 8.28 1.88 9.8.35c.24-.23.5-.35.85-.35.32 0 .6.12.84.35l2.16 2.16c.23.25.34.53.34.85z"/>
+        </svg>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/DeleteIcon.js b/server/sonar-web/src/main/js/apps/projectActivity/components/DeleteIcon.js
new file mode 100644 (file)
index 0000000..fc0ea42
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+
+export default class DeleteIcon extends React.Component {
+  render () {
+    /* eslint-disable max-len */
+    return (
+        <svg width="12" height="12" viewBox="0 0 14 14">
+          <path fill="#d4333f"
+                d="M14 11.27c0 .3-.1.58-.33.8l-1.6 1.6c-.22.22-.5.33-.8.33-.32 0-.6-.1-.8-.33L7 10.2l-3.46 3.47c-.22.22-.5.33-.8.33-.32 0-.6-.1-.8-.33l-1.6-1.6c-.23-.22-.34-.5-.34-.8 0-.32.1-.6.33-.8L3.8 7 .32 3.54C.1 3.32 0 3.04 0 2.74c0-.32.1-.6.33-.8l1.6-1.6c.22-.23.5-.34.8-.34.32 0 .6.1.8.33L7 3.8 10.46.32c.22-.22.5-.33.8-.33.32 0 .6.1.8.33l1.6 1.6c.23.22.34.5.34.8 0 .32-.1.6-.33.8L10.2 7l3.47 3.46c.22.22.33.5.33.8z"/>
+        </svg>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.css b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.css
new file mode 100644 (file)
index 0000000..0a28479
--- /dev/null
@@ -0,0 +1,7 @@
+.project-activity-event {
+
+}
+
+.project-activity-event + .project-activity-event {
+  margin-top: 4px;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js
new file mode 100644 (file)
index 0000000..cb0f236
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import EventInner from './EventInner';
+import ChangeCustomEventForm from './forms/ChangeCustomEventForm';
+import RemoveCustomEventForm from './forms/RemoveCustomEventForm';
+import DeleteIcon from './DeleteIcon';
+import ChangeIcon from './ChangeIcon';
+import type { Event as EventType } from '../../../store/projectActivity/duck';
+
+type Props = {
+  analysis: string,
+  event: EventType,
+  isFirst: boolean,
+  canAdmin: boolean
+};
+
+type State = {
+  changing: boolean,
+  deleting: boolean
+};
+
+export default class Event extends React.Component {
+  mounted: boolean;
+  props: Props;
+
+  state: State = {
+    changing: false,
+    deleting: false
+  };
+
+  componentDidMount () {
+    this.mounted = true;
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  startChanging = () => {
+    this.setState({ changing: true });
+  };
+
+  stopChanging = () => {
+    if (this.mounted) {
+      this.setState({ changing: false });
+    }
+  };
+
+  startDeleting = () => {
+    this.setState({ deleting: true });
+  };
+
+  stopDeleting = () => {
+    if (this.mounted) {
+      this.setState({ deleting: false });
+    }
+  };
+
+  render () {
+    const { event, canAdmin } = this.props;
+    const canChange = ['OTHER', 'VERSION'].includes(event.category);
+    const canDelete = event.category === 'OTHER' || (event.category === 'VERSION' && !this.props.isFirst);
+    const showActions = canAdmin && (canChange || canDelete);
+
+    return (
+        <div className="project-activity-event">
+          <EventInner event={this.props.event}/>
+
+          {showActions && (
+              <div className="project-activity-event-actions">
+                {canChange && (
+                    <button className="js-change-event button-clean" onClick={this.startChanging}>
+                      <ChangeIcon/>
+                    </button>
+                )}
+                {canDelete && (
+                    <button className="js-delete-event button-clean" onClick={this.startDeleting}>
+                      <DeleteIcon/>
+                    </button>
+                )}
+              </div>
+          )}
+
+          {this.state.changing && (
+              <ChangeCustomEventForm
+                  event={this.props.event}
+                  onClose={this.stopChanging}/>
+          )}
+
+          {this.state.deleting && (
+              <RemoveCustomEventForm
+                  analysis={this.props.analysis}
+                  event={this.props.event}
+                  onClose={this.stopDeleting}/>
+          )}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js
new file mode 100644 (file)
index 0000000..61dbfa3
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import type { Event as EventType } from '../../../store/projectActivity/duck';
+import { translate } from '../../../helpers/l10n';
+import './Event.css';
+
+export default class EventInner extends React.Component {
+  props: {
+    event: EventType
+  };
+
+  render () {
+    const { event } = this.props;
+
+    if (event.category === 'VERSION') {
+      return (
+          <span className="badge project-activity-version-badge">{this.props.event.name}</span>
+      );
+    }
+
+    return (
+        <span>
+          <span className="note">{translate('event.category', event.category)}:</span>
+          {' '}
+          <strong title={event.description}>{event.name}</strong>
+        </span>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js
new file mode 100644 (file)
index 0000000..7452f26
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import sortBy from 'lodash/sortBy';
+import Event from './Event';
+import type { Event as EventType } from '../../../store/projectActivity/duck';
+
+export default class Events extends React.Component {
+  props: {
+    analysis: string,
+    events: Array<EventType>,
+    isFirst: boolean,
+    canAdmin: boolean
+  };
+
+  render () {
+    const sortedEvents: Array<EventType> = sortBy(
+        this.props.events,
+        // versions first
+        (event: EventType) => event.category === 'VERSION' ? 0 : 1,
+        // then the rest sorted by category
+        'category'
+    );
+
+    return (
+        <div className="project-activity-events">
+          {sortedEvents.map(event => (
+              <Event
+                  key={event.key}
+                  analysis={this.props.analysis}
+                  event={event}
+                  isFirst={this.props.isFirst}
+                  canAdmin={this.props.canAdmin}/>
+          ))}
+        </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
new file mode 100644 (file)
index 0000000..91b827d
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import groupBy from 'lodash/groupBy';
+import moment from 'moment';
+import ProjectActivityAnalysis from './ProjectActivityAnalysis';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import { getProjectActivity } from '../../../store/rootReducer';
+import { getAnalyses } from '../../../store/projectActivity/duck';
+import { translate } from '../../../helpers/l10n';
+
+class ProjectActivityAnalysesList extends React.Component {
+  props: {
+    project: string,
+    analyses?: Array<{
+      key: string,
+      date: string
+    }>,
+    canAdmin: boolean
+  };
+
+  render () {
+    if (!this.props.analyses) {
+      return null;
+    }
+
+    if (this.props.analyses.length === 0) {
+      return (
+          <div className="note">{translate('no_results')}</div>
+      );
+    }
+
+    const firstAnalysis = this.props.analyses[0];
+
+    const byDay = groupBy(this.props.analyses, analysis => moment(analysis.date).startOf('day').valueOf());
+
+    return (
+        <div className="boxed-group boxed-group-inner">
+          <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].map(analysis => (
+                        <ProjectActivityAnalysis
+                            key={analysis.key}
+                            analysis={analysis}
+                            isFirst={analysis === firstAnalysis}
+                            project={this.props.project}
+                            canAdmin={this.props.canAdmin}/>
+                    ))}
+                  </ul>
+                </li>
+            ))}
+          </ul>
+        </div>
+    );
+  }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+  analyses: getAnalyses(getProjectActivity(state), ownProps.project)
+});
+
+export default connect(mapStateToProps)(ProjectActivityAnalysesList);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
new file mode 100644 (file)
index 0000000..23e6915
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import Events from './Events';
+import AddVersionForm from './forms/AddVersionForm';
+import AddCustomEventForm from './forms/AddCustomEventForm';
+import RemoveAnalysisForm from './forms/RemoveAnalysisForm';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import type { Analysis } from '../../../store/projectActivity/duck';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProjectActivityAnalysis extends React.Component {
+  props: {
+    analysis: Analysis,
+    isFirst: boolean,
+    project: string,
+    canAdmin: boolean
+  };
+
+  render () {
+    const { date, events } = this.props.analysis;
+    const { isFirst, canAdmin } = this.props;
+
+    const version = events.find(event => event.category === 'VERSION');
+
+    return (
+        <li className="project-activity-analysis clearfix">
+          {canAdmin && (
+              <div className="project-activity-analysis-actions">
+                <div className="dropdown display-inline-block">
+                  <button className="js-create button-small" data-toggle="dropdown">
+                    {translate('create')} <i className="icon-dropdown"/>
+                  </button>
+                  <ul className="dropdown-menu dropdown-menu-right">
+                    {version == null && (
+                        <li>
+                          <AddVersionForm analysis={this.props.analysis}/>
+                        </li>
+                    )}
+                    <li>
+                      <AddCustomEventForm analysis={this.props.analysis}/>
+                    </li>
+                  </ul>
+                </div>
+
+                {!isFirst && (
+                    <div className="display-inline-block little-spacer-left">
+                      <RemoveAnalysisForm
+                          analysis={this.props.analysis}
+                          project={this.props.project}/>
+                    </div>
+                )}
+              </div>
+          )}
+
+          <div className="project-activity-time">
+            <FormattedDate date={date} format="LT" tooltipFormat="LTS"/>
+          </div>
+
+          {events.length > 0 && (
+              <Events
+                  analysis={this.props.analysis.key}
+                  events={events}
+                  isFirst={this.props.isFirst}
+                  canAdmin={canAdmin}/>
+          )}
+        </li>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
new file mode 100644 (file)
index 0000000..6dc2418
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import ProjectActivityPageHeader from './ProjectActivityPageHeader';
+import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
+import ProjectActivityPageFooter from './ProjectActivityPageFooter';
+import { fetchProjectActivity } from '../actions';
+import { getComponent } from '../../../store/rootReducer';
+import './projectActivity.css';
+
+type Props = {
+  location: { query: { id: string } },
+  fetchProjectActivity: (project: string) => void,
+  filter: ?string,
+  project: { configuration?: { showHistory: boolean } }
+};
+
+type State = {
+  filter: ?string
+};
+
+class ProjectActivityApp extends React.Component {
+  props: Props;
+
+  state: State = {
+    filter: null
+  };
+
+  componentDidMount () {
+    document.querySelector('html').classList.add('dashboard-page');
+    this.props.fetchProjectActivity(this.props.location.query.id);
+  }
+
+  componentWillUnmount () {
+    document.querySelector('html').classList.remove('dashboard-page');
+  }
+
+  handleFilter = (filter: ?string) => {
+    this.setState({ filter });
+    this.props.fetchProjectActivity(this.props.location.query.id, filter);
+  };
+
+  render () {
+    const project = this.props.location.query.id;
+    const { configuration } = this.props.project;
+    const canAdmin = configuration ? configuration.showHistory : false;
+
+    return (
+        <div id="project-activity" className="page page-limited">
+          <ProjectActivityPageHeader
+              project={project}
+              filter={this.state.filter}
+              changeFilter={this.handleFilter}/>
+
+          <ProjectActivityAnalysesList
+              project={project}
+              canAdmin={canAdmin}/>
+
+          <ProjectActivityPageFooter
+              project={project}/>
+        </div>
+    );
+  }
+}
+
+const mapStateToProps = (state, ownProps: Props) => ({
+  project: getComponent(state, ownProps.location.query.id)
+});
+
+const mapDispatchToProps = { fetchProjectActivity };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ProjectActivityApp);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js
new file mode 100644 (file)
index 0000000..a487c99
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import ListFooter from '../../../components/controls/ListFooter';
+import { getProjectActivity } from '../../../store/rootReducer';
+import { getAnalyses, getPaging } from '../../../store/projectActivity/duck';
+import { fetchMoreProjectActivity } from '../actions';
+import type { Paging } from '../../../store/projectActivity/duck';
+
+class ProjectActivityPageFooter extends React.Component {
+  props: {
+    analyses: Array<*>,
+    paging: ?Paging,
+    project: string,
+    fetchMoreProjectActivity: (project: string) => void
+  };
+
+  handleLoadMore = () => {
+    this.props.fetchMoreProjectActivity(this.props.project);
+  };
+
+  render () {
+    const { analyses, paging } = this.props;
+
+    if (!paging || analyses.length === 0) {
+      return null;
+    }
+
+    return (
+        <ListFooter count={analyses.length} total={paging.total} loadMore={this.handleLoadMore}/>
+    );
+  }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+  analyses: getAnalyses(getProjectActivity(state), ownProps.project),
+  paging: getPaging(getProjectActivity(state), ownProps.project)
+});
+
+const mapDispatchToProps = { fetchMoreProjectActivity };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ProjectActivityPageFooter);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
new file mode 100644 (file)
index 0000000..091a029
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import Select from 'react-select';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+  changeFilter: (filter: ?string) => void,
+  filter: ?string,
+  project: string
+};
+
+export default class ProjectActivityPageHeader extends React.Component {
+  props: Props;
+
+  handleChange = (option: null | { value: string }) => {
+    this.props.changeFilter(option && option.value);
+  }
+
+  render () {
+    const selectOptions = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'].map(category => ({
+      label: translate('event.category', category),
+      value: category
+    }));
+
+    return (
+        <header className="page-header">
+          <div className="page-actions">
+            <Select
+                className="input-medium"
+                placeholder={translate('filter_verb') + '...'}
+                clearable={true}
+                searchable={false}
+                value={this.props.filter}
+                options={selectOptions}
+                onChange={this.handleChange}/>
+          </div>
+
+          <div className="page-description">
+            {translate('project_activity.page.description')}
+          </div>
+        </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js
new file mode 100644 (file)
index 0000000..4d4f287
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import { addCustomEvent } from '../../actions';
+import AddEventForm from './AddEventForm';
+
+const AddCustomEventForm = props => (
+    <AddEventForm {...props} addEventButtonText="project_activity.add_custom_event"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { addEvent: addCustomEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddCustomEventForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
new file mode 100644 (file)
index 0000000..da895b6
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import type { Analysis } from '../../../../store/projectActivity/duck';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {
+  addEvent: () => Promise<*>,
+  analysis: Analysis,
+  addEventButtonText: string
+};
+
+type State = {
+  open: boolean,
+  processing: boolean;
+  name: string;
+}
+
+export default class AddEventForm extends React.Component {
+  mounted: boolean;
+  props: Props;
+
+  state: State = {
+    open: false,
+    processing: false,
+    name: ''
+  };
+
+  componentDidMount () {
+    this.mounted = true;
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  openForm = (e: Object) => {
+    e.preventDefault();
+    if (this.mounted) {
+      this.setState({ open: true });
+    }
+  };
+
+  closeForm = () => {
+    if (this.mounted) {
+      this.setState({ open: false, name: '' });
+    }
+  };
+
+  changeInput = (e: Object) => {
+    if (this.mounted) {
+      this.setState({ name: e.target.value });
+    }
+  };
+
+  stopProcessing = () => {
+    if (this.mounted) {
+      this.setState({ processing: false });
+    }
+  };
+
+  stopProcessingAndClose = () => {
+    if (this.mounted) {
+      this.setState({ open: false, processing: false, name: '' });
+    }
+  };
+
+  handleSubmit = (e: Object) => {
+    e.preventDefault();
+    this.setState({ processing: true });
+    this.props.addEvent(this.props.analysis.key, this.state.name)
+        .then(this.stopProcessingAndClose, this.stopProcessing);
+  };
+
+  renderModal () {
+    return (
+        <Modal isOpen={true}
+               contentLabel="modal form"
+               className="modal"
+               overlayClassName="modal-overlay"
+               onRequestClose={this.closeForm}>
+
+          <header className="modal-head">
+            <h2>{translate(this.props.addEventButtonText)}</h2>
+          </header>
+
+          <form onSubmit={this.handleSubmit}>
+            <div className="modal-body">
+              <div className="modal-field">
+                <label>{translate('name')}</label>
+                <input
+                    value={this.state.name}
+                    autoFocus={true}
+                    disabled={this.state.processing}
+                    className="input-medium"
+                    type="text"
+                    onChange={this.changeInput}/>
+              </div>
+            </div>
+
+            <footer className="modal-foot">
+              {this.state.processing ? (
+                      <i className="spinner"/>
+                  ) : (
+                      <div>
+                        <button type="submit">{translate('save')}</button>
+                        <button type="reset" className="button-link" onClick={this.closeForm}>
+                          {translate('cancel')}
+                        </button>
+                      </div>
+                  )}
+            </footer>
+          </form>
+
+        </Modal>
+    );
+  }
+
+  render () {
+    return (
+        <a className="js-add-event button-small" href="#" onClick={this.openForm}>
+          {translate(this.props.addEventButtonText)}
+          {this.state.open && this.renderModal()}
+        </a>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js
new file mode 100644 (file)
index 0000000..8443d0b
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import { addVersion } from '../../actions';
+import AddEventForm from './AddEventForm';
+
+const AddVersionForm = props => (
+    <AddEventForm {...props} addEventButtonText="project_activity.add_version"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { addEvent: addVersion };
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddVersionForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js
new file mode 100644 (file)
index 0000000..df4a93e
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import ChangeEventForm from './ChangeEventForm';
+import { changeEvent } from '../../actions';
+
+const ChangeCustomEventForm = props => (
+    <ChangeEventForm
+        {...props}
+        changeEventButtonText="project_activity.change_custom_event"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { changeEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ChangeCustomEventForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js
new file mode 100644 (file)
index 0000000..8ae9e6b
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import type { Event } from '../../../../store/projectActivity/duck';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {
+  changeEvent: () => Promise<*>,
+  changeEventButtonText: string,
+  event: Event,
+  onClose: () => void
+};
+
+type State = {
+  processing: boolean,
+  name: string
+}
+
+export default class ChangeEventForm extends React.Component {
+  mounted: boolean;
+  props: Props;
+  state: State;
+
+  constructor (props: Props) {
+    super(props);
+    this.state = {
+      processing: false,
+      name: props.event.name
+    };
+  }
+
+  componentDidMount () {
+    this.mounted = true;
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  closeForm = () => {
+    if (this.mounted) {
+      this.setState({ name: this.props.event.name });
+    }
+    this.props.onClose();
+  };
+
+  changeInput = (e: Object) => {
+    if (this.mounted) {
+      this.setState({ name: e.target.value });
+    }
+  };
+
+  stopProcessing = () => {
+    if (this.mounted) {
+      this.setState({ processing: false });
+    }
+  };
+
+  stopProcessingAndClose = () => {
+    if (this.mounted) {
+      this.setState({ processing: false });
+    }
+    this.props.onClose();
+  };
+
+  handleSubmit = (e: Object) => {
+    e.preventDefault();
+    this.setState({ processing: true });
+    this.props.changeEvent(this.props.event.key, this.state.name)
+        .then(this.stopProcessingAndClose, this.stopProcessing);
+  };
+
+  render () {
+    return (
+        <Modal isOpen={true}
+               contentLabel="modal form"
+               className="modal"
+               overlayClassName="modal-overlay"
+               onRequestClose={this.closeForm}>
+
+          <header className="modal-head">
+            <h2>{translate(this.props.changeEventButtonText)}</h2>
+          </header>
+
+          <form onSubmit={this.handleSubmit}>
+            <div className="modal-body">
+              <div className="modal-field">
+                <label>{translate('name')}</label>
+                <input
+                    value={this.state.name}
+                    autoFocus={true}
+                    disabled={this.state.processing}
+                    className="input-medium"
+                    type="text"
+                    onChange={this.changeInput}/>
+              </div>
+            </div>
+
+            <footer className="modal-foot">
+              {this.state.processing ? (
+                      <i className="spinner"/>
+                  ) : (
+                      <div>
+                        <button type="submit">{translate('change_verb')}</button>
+                        <button type="reset" className="button-link" onClick={this.closeForm}>
+                          {translate('cancel')}
+                        </button>
+                      </div>
+                  )}
+            </footer>
+          </form>
+
+        </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js
new file mode 100644 (file)
index 0000000..e4a088d
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import ChangeEventForm from './ChangeEventForm';
+import { changeEvent } from '../../actions';
+
+const ChangeVersionForm = props => (
+    <ChangeEventForm
+        {...props}
+        changeEventButtonText="project_activity.change_version"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { changeEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ChangeVersionForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
new file mode 100644 (file)
index 0000000..4823627
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import Modal from 'react-modal';
+import type { Analysis } from '../../../../store/projectActivity/duck';
+import { translate } from '../../../../helpers/l10n';
+import { deleteAnalysis } from '../../actions';
+
+type Props = {
+  analysis: Analysis,
+  deleteAnalysis: () => Promise<*>,
+  project: string
+};
+
+type State = {
+  open: boolean,
+  processing: boolean
+}
+
+class RemoveAnalysisForm extends React.Component {
+  mounted: boolean;
+  props: Props;
+
+  state: State = {
+    open: false,
+    processing: false
+  };
+
+  componentDidMount () {
+    this.mounted = true;
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  openForm = () => {
+    if (this.mounted) {
+      this.setState({ open: true });
+    }
+  };
+
+  closeForm = () => {
+    if (this.mounted) {
+      this.setState({ open: false });
+    }
+  };
+
+  stopProcessing = () => {
+    if (this.mounted) {
+      this.setState({ processing: false });
+    }
+  };
+
+  stopProcessingAndClose = () => {
+    if (this.mounted) {
+      this.setState({ open: false, processing: false });
+    }
+  };
+
+  handleSubmit = (e: Object) => {
+    e.preventDefault();
+    this.setState({ processing: true });
+    this.props.deleteAnalysis(this.props.project, this.props.analysis.key)
+        .then(this.stopProcessingAndClose, this.stopProcessing);
+  };
+
+  renderModal () {
+    return (
+        <Modal isOpen={true}
+               contentLabel="modal form"
+               className="modal"
+               overlayClassName="modal-overlay"
+               onRequestClose={this.closeForm}>
+
+          <header className="modal-head">
+            <h2>{translate('project_activity.delete_analysis')}</h2>
+          </header>
+
+          <form onSubmit={this.handleSubmit}>
+            <div className="modal-body">
+              {translate('project_activity.delete_analysis.question')}
+            </div>
+
+            <footer className="modal-foot">
+              {this.state.processing ? (
+                      <i className="spinner"/>
+                  ) : (
+                      <div>
+                        <button type="submit" className="button-red">{translate('delete')}</button>
+                        <button type="reset" className="button-link" onClick={this.closeForm}>
+                          {translate('cancel')}
+                        </button>
+                      </div>
+                  )}
+            </footer>
+          </form>
+
+        </Modal>
+    );
+  }
+
+  render () {
+    return (
+        <button className="js-delete-analysis button-small button-red" onClick={this.openForm}>
+          {translate('delete')}
+          {this.state.open && this.renderModal()}
+        </button>
+    );
+  }
+}
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { deleteAnalysis };
+
+export default connect(mapStateToProps, mapDispatchToProps)(RemoveAnalysisForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js
new file mode 100644 (file)
index 0000000..e9679b0
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import RemoveEventForm from './RemoveEventForm';
+import { deleteEvent } from '../../actions';
+
+const RemoveCustomEventForm = props => (
+    <RemoveEventForm
+        {...props}
+        removeEventButtonText="project_activity.remove_custom_event"
+        removeEventQuestion="project_activity.remove_custom_event.question"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { deleteEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(RemoveCustomEventForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js
new file mode 100644 (file)
index 0000000..893d164
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import type { Analysis, Event } from '../../../../store/projectActivity/duck';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {
+  analysis: Analysis,
+  deleteEvent: () => Promise<*>,
+  event: Event,
+  removeEventButtonText: string,
+  removeEventQuestion: string,
+  onClose: () => void
+};
+
+type State = {
+  processing: boolean
+}
+
+export default class RemoveVersionForm extends React.Component {
+  mounted: boolean;
+  props: Props;
+
+  state: State = {
+    processing: false
+  };
+
+  componentDidMount () {
+    this.mounted = true;
+  }
+
+  componentWillUnmount () {
+    this.mounted = false;
+  }
+
+  closeForm = () => {
+    this.props.onClose();
+  };
+
+  stopProcessing = () => {
+    if (this.mounted) {
+      this.setState({ processing: false });
+    }
+  };
+
+  stopProcessingAndClose = () => {
+    if (this.mounted) {
+      this.setState({ processing: false });
+    }
+    this.props.onClose();
+  };
+
+  handleSubmit = (e: Object) => {
+    e.preventDefault();
+    this.setState({ processing: true });
+    this.props.deleteEvent(this.props.analysis, this.props.event.key)
+        .then(this.stopProcessingAndClose, this.stopProcessing);
+  };
+
+  render () {
+    return (
+        <Modal isOpen={true}
+               contentLabel="modal form"
+               className="modal"
+               overlayClassName="modal-overlay"
+               onRequestClose={this.closeForm}>
+
+          <header className="modal-head">
+            <h2>{translate(this.props.removeEventButtonText)}</h2>
+          </header>
+
+          <form onSubmit={this.handleSubmit}>
+            <div className="modal-body">
+              {translate(this.props.removeEventQuestion)}
+            </div>
+
+            <footer className="modal-foot">
+              {this.state.processing ? (
+                      <i className="spinner"/>
+                  ) : (
+                      <div>
+                        <button type="submit" className="button-red" autoFocus={true}>{translate('delete')}</button>
+                        <button type="reset" className="button-link" onClick={this.closeForm}>
+                          {translate('cancel')}
+                        </button>
+                      </div>
+                  )}
+            </footer>
+          </form>
+
+        </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js
new file mode 100644 (file)
index 0000000..8d1d6bc
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import RemoveEventForm from './RemoveEventForm';
+import { deleteEvent } from '../../actions';
+
+const RemoveVersionForm = props => (
+    <RemoveEventForm
+        {...props}
+        removeEventButtonText="project_activity.remove_version"
+        removeEventQuestion="project_activity.remove_version.question"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { deleteEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(RemoveVersionForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
new file mode 100644 (file)
index 0000000..79e426e
--- /dev/null
@@ -0,0 +1,121 @@
+.project-activity-days-list {
+
+}
+
+.project-activity-day {
+  margin-bottom: 40px;
+}
+
+.project-activity-date {
+  margin-bottom: 16px;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+.project-activity-analyses-list {
+
+}
+
+.project-activity-analysis {
+  position: relative;
+  min-height: 20px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+  border-top: 1px solid #e6e6e6;
+  border-bottom: 1px solid #e6e6e6;
+}
+
+.project-activity-analysis:hover {
+  background-color: #ecf6fe;
+}
+
+.project-activity-analysis + .project-activity-analysis {
+  border-top: none;
+}
+
+.project-activity-analysis-actions {
+  float: right;
+  padding-right: 10px;
+}
+
+.project-activity-analysis-actions:first-child,
+.project-activity-analysis-actions:empty {
+  margin-top: 0;
+}
+
+.project-activity-analysis-actions > button + button,
+.project-activity-analysis-actions > button + form,
+.project-activity-analysis-actions > form + button,
+.project-activity-analysis-actions > form + form {
+  margin-left: 8px;
+}
+
+.project-activity-analysis-form {
+  display: inline-block;
+  vertical-align: top;
+  line-height: 20px;
+  margin-bottom: 10px;
+  padding: 9px;
+  border: 1px solid #faebcc;
+  border-radius: 2px;
+  background-color: #fcf8e3;
+}
+
+.project-activity-analysis-form + .project-activity-analysis-form {
+  margin-left: 8px;
+}
+
+.project-activity-time {
+  float: left;
+  width: 130px;
+  line-height: 20px;
+  padding-right: 50px;
+  box-sizing: border-box;
+  font-size: 12px;
+  font-weight: bold;
+  text-align: right;
+}
+
+.project-activity-time::after {
+  position: absolute;
+  z-index: 21;
+  top: 11px;
+  left: 100px;
+  display: block;
+  width: 10px;
+  height: 10px;
+  border: 2px solid #4b9fd5;
+  border-radius: 10px;
+  box-sizing: border-box;
+  content: "";
+}
+
+.project-activity-events {
+  overflow: hidden;
+}
+
+.project-activity-event {
+  line-height: 20px;
+}
+
+.project-activity-event-actions {
+  display: inline-block;
+  margin-left: 8px;
+}
+
+.project-activity-event-actions button {
+  height: 20px;
+}
+
+.project-activity-event-actions button + button {
+  margin-left: 4px;
+}
+
+.project-activity-version-badge {
+  vertical-align: middle;
+  padding: 4px 8px;
+  border-radius: 2px;
+  font-weight: bold;
+  font-size: 12px;
+  letter-spacing: 0;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/routes.js b/server/sonar-web/src/main/js/apps/projectActivity/routes.js
new file mode 100644 (file)
index 0000000..a6bb1db
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import { IndexRoute } from 'react-router';
+import ProjectActivityApp from './components/ProjectActivityApp';
+
+export default (
+    <IndexRoute component={ProjectActivityApp}/>
+);
diff --git a/server/sonar-web/src/main/js/components/ui/FormattedDate.js b/server/sonar-web/src/main/js/components/ui/FormattedDate.js
new file mode 100644 (file)
index 0000000..7794976
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import React from 'react';
+import moment from 'moment';
+
+export default class FormattedDate extends React.Component {
+  props: {
+    date: string | number,
+    format?: string,
+    tooltipFormat?: string
+  };
+
+  static defaultProps = {
+    format: 'LLL'
+  };
+
+  render () {
+    const { date, format, tooltipFormat } = this.props;
+
+    const m = moment(date);
+
+    const title = tooltipFormat ? m.format(tooltipFormat) : undefined;
+
+    return (
+        <time dateTime={m.format()} title={title}>
+          {m.format(format)}
+        </time>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap
new file mode 100644 (file)
index 0000000..2f807df
--- /dev/null
@@ -0,0 +1,93 @@
+exports[`test reducer 1`] = `Object {}`;
+
+exports[`test reducer 2`] = `
+Object {
+  "AVgAgC1Vdo07z3PUnnkt": Object {
+    "date": "2016-10-26T12:17:29+0200",
+    "events": Array [
+      "AVkWNYNYr4pSN7TrXcjY"
+    ],
+    "key": "AVgAgC1Vdo07z3PUnnkt"
+  },
+  "AVgFqeOSKpGuA48ADATE": Object {
+    "date": "2016-10-27T12:21:15+0200",
+    "events": Array [],
+    "key": "AVgFqeOSKpGuA48ADATE"
+  },
+  "AVgGkRvCrrTJiPpCD-rG": Object {
+    "date": "2016-10-27T16:33:50+0200",
+    "events": Array [
+      "AVjUDBiSiXOcXjpycvde"
+    ],
+    "key": "AVgGkRvCrrTJiPpCD-rG"
+  }
+}
+`;
+
+exports[`test reducer 3`] = `
+Object {
+  "AVgAgC1Vdo07z3PUnnkt": Object {
+    "date": "2016-10-26T12:17:29+0200",
+    "events": Array [
+      "AVkWNYNYr4pSN7TrXcjY"
+    ],
+    "key": "AVgAgC1Vdo07z3PUnnkt"
+  },
+  "AVgFqeOSKpGuA48ADATE": Object {
+    "date": "2016-10-27T12:21:15+0200",
+    "events": Array [],
+    "key": "AVgFqeOSKpGuA48ADATE"
+  },
+  "AVgGkRvCrrTJiPpCD-rG": Object {
+    "date": "2016-10-27T16:33:50+0200",
+    "events": Array [
+      "AVjUDBiSiXOcXjpycvde",
+      "AVkWcQ8Hr4pSN7TrXcjZ"
+    ],
+    "key": "AVgGkRvCrrTJiPpCD-rG"
+  }
+}
+`;
+
+exports[`test reducer 4`] = `
+Object {
+  "AVgAgC1Vdo07z3PUnnkt": Object {
+    "date": "2016-10-26T12:17:29+0200",
+    "events": Array [
+      "AVkWNYNYr4pSN7TrXcjY"
+    ],
+    "key": "AVgAgC1Vdo07z3PUnnkt"
+  },
+  "AVgFqeOSKpGuA48ADATE": Object {
+    "date": "2016-10-27T12:21:15+0200",
+    "events": Array [],
+    "key": "AVgFqeOSKpGuA48ADATE"
+  },
+  "AVgGkRvCrrTJiPpCD-rG": Object {
+    "date": "2016-10-27T16:33:50+0200",
+    "events": Array [
+      "AVjUDBiSiXOcXjpycvde"
+    ],
+    "key": "AVgGkRvCrrTJiPpCD-rG"
+  }
+}
+`;
+
+exports[`test reducer 5`] = `
+Object {
+  "AVgAgC1Vdo07z3PUnnkt": Object {
+    "date": "2016-10-26T12:17:29+0200",
+    "events": Array [
+      "AVkWNYNYr4pSN7TrXcjY"
+    ],
+    "key": "AVgAgC1Vdo07z3PUnnkt"
+  },
+  "AVgGkRvCrrTJiPpCD-rG": Object {
+    "date": "2016-10-27T16:33:50+0200",
+    "events": Array [
+      "AVjUDBiSiXOcXjpycvde"
+    ],
+    "key": "AVgGkRvCrrTJiPpCD-rG"
+  }
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap
new file mode 100644 (file)
index 0000000..f4fafe2
--- /dev/null
@@ -0,0 +1,55 @@
+exports[`test reducer 1`] = `Object {}`;
+
+exports[`test reducer 2`] = `
+Object {
+  "project-foo": Array [
+    "AVgFqeOSKpGuA48ADATE",
+    "AVgAgC1Vdo07z3PUnnkt"
+  ]
+}
+`;
+
+exports[`test reducer 3`] = `
+Object {
+  "project-foo": Array [
+    "AVgFqeOSKpGuA48ADATE",
+    "AVgAgC1Vdo07z3PUnnkt",
+    "AVgFqeOSKpGuA48ADATX"
+  ]
+}
+`;
+
+exports[`test reducer 4`] = `
+Object {
+  "project-bar": Array [
+    "AVgGkRvCrrTJiPpCD-rG"
+  ],
+  "project-foo": Array [
+    "AVgFqeOSKpGuA48ADATE",
+    "AVgAgC1Vdo07z3PUnnkt",
+    "AVgFqeOSKpGuA48ADATX"
+  ]
+}
+`;
+
+exports[`test reducer 5`] = `
+Object {
+  "project-bar": Array [
+    "AVgGkRvCrrTJiPpCD-rG"
+  ],
+  "project-foo": Array [
+    "AVgAgC1Vdo07z3PUnnkt",
+    "AVgFqeOSKpGuA48ADATX"
+  ]
+}
+`;
+
+exports[`test reducer 6`] = `
+Object {
+  "project-bar": Array [],
+  "project-foo": Array [
+    "AVgAgC1Vdo07z3PUnnkt",
+    "AVgFqeOSKpGuA48ADATX"
+  ]
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap
new file mode 100644 (file)
index 0000000..c547f46
--- /dev/null
@@ -0,0 +1,75 @@
+exports[`actions addEvent 1`] = `
+Object {
+  "analysis": "foo",
+  "event": Object {
+    "key": "bar"
+  },
+  "type": "ADD_PROJECT_ACTIVITY_EVENT"
+}
+`;
+
+exports[`actions changeEvent 1`] = `
+Object {
+  "changes": Object {
+    "name": "bar"
+  },
+  "event": "foo",
+  "type": "CHANGE_PROJECT_ACTIVITY_EVENT"
+}
+`;
+
+exports[`actions deleteAnalysis 1`] = `
+Object {
+  "analysis": "bar",
+  "project": "foo",
+  "type": "DELETE_PROJECT_ACTIVITY_ANALYSIS"
+}
+`;
+
+exports[`actions deleteEvent 1`] = `
+Object {
+  "analysis": "foo",
+  "event": "bar",
+  "type": "DELETE_PROJECT_ACTIVITY_EVENT"
+}
+`;
+
+exports[`selectors getAnalyses 1`] = `
+Array [
+  Object {
+    "date": "2016-10-27T16:33:50+0200",
+    "events": Array [
+      Object {
+        "category": "VERSION",
+        "key": "AVjUDBiSiXOcXjpycvde",
+        "name": "2.18-SNAPSHOT"
+      }
+    ],
+    "key": "AVgGkRvCrrTJiPpCD-rG"
+  },
+  Object {
+    "date": "2016-10-27T12:21:15+0200",
+    "events": Array [],
+    "key": "AVgFqeOSKpGuA48ADATE"
+  },
+  Object {
+    "date": "2016-10-26T12:17:29+0200",
+    "events": Array [
+      Object {
+        "category": "OTHER",
+        "key": "AVkWNYNYr4pSN7TrXcjY",
+        "name": "foo"
+      }
+    ],
+    "key": "AVgAgC1Vdo07z3PUnnkt"
+  }
+]
+`;
+
+exports[`selectors getPaging 1`] = `
+Object {
+  "pageIndex": 1,
+  "pageSize": 100,
+  "total": 3
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap
new file mode 100644 (file)
index 0000000..78327b2
--- /dev/null
@@ -0,0 +1,71 @@
+exports[`test reducer 1`] = `Object {}`;
+
+exports[`test reducer 2`] = `
+Object {
+  "AVjUDBiSiXOcXjpycvde": Object {
+    "category": "VERSION",
+    "key": "AVjUDBiSiXOcXjpycvde",
+    "name": "2.18-SNAPSHOT"
+  },
+  "AVkWNYNYr4pSN7TrXcjY": Object {
+    "category": "OTHER",
+    "key": "AVkWNYNYr4pSN7TrXcjY",
+    "name": "foo"
+  }
+}
+`;
+
+exports[`test reducer 3`] = `
+Object {
+  "AVjUDBiSiXOcXjpycvde": Object {
+    "category": "VERSION",
+    "key": "AVjUDBiSiXOcXjpycvde",
+    "name": "2.18-SNAPSHOT"
+  },
+  "AVkWNYNYr4pSN7TrXcjY": Object {
+    "category": "OTHER",
+    "key": "AVkWNYNYr4pSN7TrXcjY",
+    "name": "foo"
+  },
+  "AVkWcQ8Hr4pSN7TrXcjZ": Object {
+    "category": "OTHER",
+    "key": "AVkWcQ8Hr4pSN7TrXcjZ",
+    "name": "custom"
+  }
+}
+`;
+
+exports[`test reducer 4`] = `
+Object {
+  "AVjUDBiSiXOcXjpycvde": Object {
+    "category": "VERSION",
+    "key": "AVjUDBiSiXOcXjpycvde",
+    "name": "2.18-SNAPSHOT"
+  },
+  "AVkWNYNYr4pSN7TrXcjY": Object {
+    "category": "OTHER",
+    "key": "AVkWNYNYr4pSN7TrXcjY",
+    "name": "foo"
+  },
+  "AVkWcQ8Hr4pSN7TrXcjZ": Object {
+    "category": "OTHER",
+    "key": "AVkWcQ8Hr4pSN7TrXcjZ",
+    "name": "new name"
+  }
+}
+`;
+
+exports[`test reducer 5`] = `
+Object {
+  "AVjUDBiSiXOcXjpycvde": Object {
+    "category": "VERSION",
+    "key": "AVjUDBiSiXOcXjpycvde",
+    "name": "2.18-SNAPSHOT"
+  },
+  "AVkWNYNYr4pSN7TrXcjY": Object {
+    "category": "OTHER",
+    "key": "AVkWNYNYr4pSN7TrXcjY",
+    "name": "foo"
+  }
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap
new file mode 100644 (file)
index 0000000..6c9e8c7
--- /dev/null
@@ -0,0 +1,21 @@
+exports[`test reducer 1`] = `Object {}`;
+
+exports[`test reducer 2`] = `
+Object {
+  "project-foo": Object {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 3
+  }
+}
+`;
+
+exports[`test reducer 3`] = `
+Object {
+  "project-foo": Object {
+    "pageIndex": 2,
+    "pageSize": 30,
+    "total": 5
+  }
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js
new file mode 100644 (file)
index 0000000..f53081e
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { configureTestStore } from '../../utils/configureStore';
+import analyses, { getAnalysis } from '../analyses';
+import { receiveProjectActivity, addEvent, deleteEvent, deleteAnalysis } from '../duck';
+
+const PROJECT = 'project-foo';
+
+const ANALYSES = [
+  {
+    key: 'AVgGkRvCrrTJiPpCD-rG',
+    date: '2016-10-27T16:33:50+0200',
+    events: [
+      {
+        key: 'AVjUDBiSiXOcXjpycvde',
+        category: 'VERSION',
+        name: '2.18-SNAPSHOT'
+      }
+    ]
+  },
+  {
+    key: 'AVgFqeOSKpGuA48ADATE',
+    date: '2016-10-27T12:21:15+0200',
+    events: []
+  },
+  {
+    key: 'AVgAgC1Vdo07z3PUnnkt',
+    date: '2016-10-26T12:17:29+0200',
+    events: [
+      {
+        key: 'AVkWNYNYr4pSN7TrXcjY',
+        category: 'OTHER',
+        name: 'foo'
+      }
+    ]
+  }
+];
+
+const PAGING = {
+  total: 3,
+  pageIndex: 1,
+  pageSize: 100
+};
+
+const NEW_EVENT = {
+  key: 'AVkWcQ8Hr4pSN7TrXcjZ',
+  category: 'OTHER',
+  name: 'custom'
+};
+
+it('reducer', () => {
+  const store = configureTestStore(analyses);
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(addEvent(ANALYSES[0].key, NEW_EVENT));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(deleteEvent(ANALYSES[0].key, NEW_EVENT.key));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(deleteAnalysis(PROJECT, ANALYSES[1].key));
+  expect(store.getState()).toMatchSnapshot();
+});
+
+it('selector `getAnalysis`', () => {
+  const analysis = ANALYSES[0];
+  const store = configureTestStore(analyses, { [analysis.key]: analysis });
+  expect(getAnalysis(store.getState(), analysis.key)).toBe(analysis);
+  expect(getAnalysis(store.getState(), 'random')).toBeFalsy();
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js
new file mode 100644 (file)
index 0000000..c374065
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { configureTestStore } from '../../utils/configureStore';
+import analysesByProject from '../analysesByProject';
+import { receiveProjectActivity, deleteAnalysis } from '../duck';
+
+const PROJECT_FOO = 'project-foo';
+const PROJECT_BAR = 'project-bar';
+
+const ANALYSES_FOO = [
+  {
+    key: 'AVgFqeOSKpGuA48ADATE',
+    date: '2016-10-27T12:21:15+0200',
+    events: []
+  },
+  {
+    key: 'AVgAgC1Vdo07z3PUnnkt',
+    date: '2016-10-26T12:17:29+0200',
+    events: []
+  }
+];
+
+const ANALYSES_FOO_2 = [
+  {
+    key: 'AVgFqeOSKpGuA48ADATX',
+    date: '2016-10-27T12:21:15+0200',
+    events: []
+  }
+];
+
+const ANALYSES_BAR = [
+  {
+    key: 'AVgGkRvCrrTJiPpCD-rG',
+    date: '2016-10-27T16:33:50+0200',
+    events: []
+  }
+];
+
+const PAGING = {
+  total: 3,
+  pageIndex: 1,
+  pageSize: 100
+};
+
+it('reducer', () => {
+  const store = configureTestStore(analysesByProject);
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(receiveProjectActivity(PROJECT_FOO, ANALYSES_FOO, PAGING));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(receiveProjectActivity(PROJECT_FOO, ANALYSES_FOO_2, { pageIndex: 2 }));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(receiveProjectActivity(PROJECT_BAR, ANALYSES_BAR, PAGING));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(deleteAnalysis(PROJECT_FOO, 'AVgFqeOSKpGuA48ADATE'));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(deleteAnalysis(PROJECT_BAR, 'AVgGkRvCrrTJiPpCD-rG'));
+  expect(store.getState()).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js
new file mode 100644 (file)
index 0000000..5a76284
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { configureTestStore } from '../../utils/configureStore';
+import reducer, {
+  receiveProjectActivity,
+  getAnalyses,
+  getPaging,
+  addEvent,
+  changeEvent,
+  deleteEvent,
+  deleteAnalysis
+} from '../duck';
+
+const PROJECT = 'project-foo';
+
+const ANALYSES = [
+  {
+    key: 'AVgGkRvCrrTJiPpCD-rG',
+    date: '2016-10-27T16:33:50+0200',
+    events: [
+      {
+        key: 'AVjUDBiSiXOcXjpycvde',
+        category: 'VERSION',
+        name: '2.18-SNAPSHOT'
+      }
+    ]
+  },
+  {
+    key: 'AVgFqeOSKpGuA48ADATE',
+    date: '2016-10-27T12:21:15+0200',
+    events: []
+  },
+  {
+    key: 'AVgAgC1Vdo07z3PUnnkt',
+    date: '2016-10-26T12:17:29+0200',
+    events: [
+      {
+        key: 'AVkWNYNYr4pSN7TrXcjY',
+        category: 'OTHER',
+        name: 'foo'
+      }
+    ]
+  }
+];
+
+const PAGING = {
+  total: 3,
+  pageIndex: 1,
+  pageSize: 100
+};
+
+describe('actions', () => {
+  it('addEvent', () => {
+    expect(addEvent('foo', { key: 'bar' })).toMatchSnapshot();
+  });
+
+  it('changeEvent', () => {
+    expect(changeEvent('foo', { name: 'bar' })).toMatchSnapshot();
+  });
+
+  it('deleteEvent', () => {
+    expect(deleteEvent('foo', 'bar')).toMatchSnapshot();
+  });
+
+  it('deleteAnalysis', () => {
+    expect(deleteAnalysis('foo', 'bar')).toMatchSnapshot();
+  });
+});
+
+
+describe('selectors', () => {
+  it('getAnalyses', () => {
+    const store = configureTestStore(reducer);
+    store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING));
+    expect(getAnalyses(store.getState(), PROJECT)).toMatchSnapshot();
+  });
+
+  it('getPaging', () => {
+    const store = configureTestStore(reducer);
+    store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING));
+    expect(getPaging(store.getState(), PROJECT)).toMatchSnapshot();
+  });
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js
new file mode 100644 (file)
index 0000000..fc95135
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { configureTestStore } from '../../utils/configureStore';
+import events, { getEvent } from '../events';
+import { receiveProjectActivity, addEvent, changeEvent, deleteEvent } from '../duck';
+
+const PROJECT = 'project-foo';
+
+const ANALYSES = [
+  {
+    key: 'AVgGkRvCrrTJiPpCD-rG',
+    date: '2016-10-27T16:33:50+0200',
+    events: [
+      {
+        key: 'AVjUDBiSiXOcXjpycvde',
+        category: 'VERSION',
+        name: '2.18-SNAPSHOT'
+      }
+    ]
+  },
+  {
+    key: 'AVgFqeOSKpGuA48ADATE',
+    date: '2016-10-27T12:21:15+0200',
+    events: []
+  },
+  {
+    key: 'AVgAgC1Vdo07z3PUnnkt',
+    date: '2016-10-26T12:17:29+0200',
+    events: [
+      {
+        key: 'AVkWNYNYr4pSN7TrXcjY',
+        category: 'OTHER',
+        name: 'foo'
+      }
+    ]
+  }
+];
+
+const PAGING = {
+  total: 3,
+  pageIndex: 1,
+  pageSize: 100
+};
+
+const NEW_EVENT = {
+  key: 'AVkWcQ8Hr4pSN7TrXcjZ',
+  category: 'OTHER',
+  name: 'custom'
+};
+
+it('reducer', () => {
+  const store = configureTestStore(events);
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(addEvent(ANALYSES[0].key, NEW_EVENT));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(changeEvent(NEW_EVENT.key, { name: 'new name' }));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(deleteEvent(ANALYSES[0].key, NEW_EVENT.key));
+  expect(store.getState()).toMatchSnapshot();
+});
+
+it('selector `getEvent`', () => {
+  const event = ANALYSES[0].events[0];
+  const store = configureTestStore(events, { [event.key]: event });
+  expect(getEvent(store.getState(), event.key)).toBe(event);
+  expect(getEvent(store.getState(), 'random')).toBeFalsy();
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js
new file mode 100644 (file)
index 0000000..11f285a
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { configureTestStore } from '../../utils/configureStore';
+import paging from '../paging';
+import { receiveProjectActivity } from '../duck';
+
+const PROJECT = 'project-foo';
+
+const ANALYSES = [];
+
+const PAGING_1 = {
+  total: 3,
+  pageIndex: 1,
+  pageSize: 100
+};
+
+const PAGING_2 = {
+  total: 5,
+  pageIndex: 2,
+  pageSize: 30
+};
+
+it('reducer', () => {
+  const store = configureTestStore(paging);
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING_1));
+  expect(store.getState()).toMatchSnapshot();
+
+  store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING_2));
+  expect(store.getState()).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/analyses.js b/server/sonar-web/src/main/js/store/projectActivity/analyses.js
new file mode 100644 (file)
index 0000000..b60abff
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import keyBy from 'lodash/keyBy';
+import type {
+  Action,
+  ReceiveProjectActivityAction,
+  AddEventAction,
+  DeleteEventAction,
+  DeleteAnalysisAction
+} from './duck';
+
+type Analysis = {
+  key: string;
+  date: string;
+  events: Array<string>
+};
+
+export type State = {
+  [key: string]: Analysis
+};
+
+const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => {
+  const analysesWithFlatEvents = action.analyses.map(analysis => ({
+    ...analysis,
+    events: analysis.events.map(event => event.key)
+  }));
+  return { ...state, ...keyBy(analysesWithFlatEvents, 'key') };
+};
+
+const addEvent = (state: State, action: AddEventAction): State => {
+  const analysis = state[action.analysis];
+  const newAnalysis = {
+    ...analysis,
+    events: [...analysis.events, action.event.key]
+  };
+  return { ...state, [action.analysis]: newAnalysis };
+};
+
+const deleteEvent = (state: State, action: DeleteEventAction): State => {
+  const analysis = state[action.analysis];
+  const newAnalysis = {
+    ...analysis,
+    events: analysis.events.filter(event => event !== action.event)
+  };
+  return { ...state, [action.analysis]: newAnalysis };
+};
+
+const deleteAnalysis = (state: State, action: DeleteAnalysisAction): State => {
+  const newState = { ...state };
+  delete newState[action.analysis];
+  return newState;
+};
+
+export default (state: State = {}, action: Action): State => {
+  switch (action.type) {
+    case 'RECEIVE_PROJECT_ACTIVITY':
+      return receiveProjectActivity(state, action);
+    case 'ADD_PROJECT_ACTIVITY_EVENT':
+      return addEvent(state, action);
+    case 'DELETE_PROJECT_ACTIVITY_EVENT':
+      return deleteEvent(state, action);
+    case 'DELETE_PROJECT_ACTIVITY_ANALYSIS':
+      return deleteAnalysis(state, action);
+    default:
+      return state;
+  }
+};
+
+export const getAnalysis = (state: State, key: string): Analysis => (
+    state[key]
+);
diff --git a/server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js b/server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js
new file mode 100644 (file)
index 0000000..d5eb2fc
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import type { Action, ReceiveProjectActivityAction, DeleteAnalysisAction } from './duck';
+
+export type State = {
+  [key: string]: Array<string>
+};
+
+const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => {
+  const analyses = state[action.project] || [];
+  const newAnalyses = action.analyses.map(analysis => analysis.key);
+  return {
+    ...state,
+    [action.project]: action.paging.pageIndex === 1 ? newAnalyses : [...analyses, ...newAnalyses]
+  };
+};
+
+const deleteAnalysis = (state: State, action: DeleteAnalysisAction): State => {
+  const analyses = state[action.project];
+  return {
+    ...state,
+    [action.project]: analyses.filter(key => key !== action.analysis)
+  };
+};
+
+export default (state: State = {}, action: Action): State => {
+  switch (action.type) {
+    case 'RECEIVE_PROJECT_ACTIVITY':
+      return receiveProjectActivity(state, action);
+    case 'DELETE_PROJECT_ACTIVITY_ANALYSIS':
+      return deleteAnalysis(state, action);
+    default:
+      return state;
+  }
+};
diff --git a/server/sonar-web/src/main/js/store/projectActivity/duck.js b/server/sonar-web/src/main/js/store/projectActivity/duck.js
new file mode 100644 (file)
index 0000000..00ddb08
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import { combineReducers } from 'redux';
+import analyses, * as fromAnalyses from './analyses';
+import type { State as AnalysesState } from './analyses';
+import analysesByProject from './analysesByProject';
+import type { State as AnalysesByProjectState } from './analysesByProject';
+import events, * as fromEvents from './events';
+import type { State as EventsState } from './events';
+import paging from './paging';
+import type { State as PagingState } from './paging';
+
+export type Event = {
+  key: string,
+  name: string;
+  category: string;
+  description?: string;
+};
+
+export type Analysis = {
+  key: string;
+  date: string;
+  events: Array<Event>
+};
+
+export type Paging = {
+  total: number,
+  pageIndex: number,
+  pageSize: number
+};
+
+export type ReceiveProjectActivityAction = {
+  type: 'RECEIVE_PROJECT_ACTIVITY',
+  project: string,
+  analyses: Array<Analysis>,
+  paging: Paging
+};
+
+export type AddEventAction = {
+  type: 'ADD_PROJECT_ACTIVITY_EVENT',
+  analysis: string,
+  event: Event
+};
+
+export type DeleteEventAction = {
+  type: 'DELETE_PROJECT_ACTIVITY_EVENT',
+  analysis: string,
+  event: string
+};
+
+export type ChangeEventAction = {
+  type: 'CHANGE_PROJECT_ACTIVITY_EVENT',
+  event: string,
+  changes: Object
+};
+
+export type DeleteAnalysisAction = {
+  type: 'DELETE_PROJECT_ACTIVITY_ANALYSIS',
+  project: string,
+  analysis: string
+};
+
+export type Action =
+    ReceiveProjectActivityAction |
+        AddEventAction |
+        DeleteEventAction |
+        ChangeEventAction |
+        DeleteAnalysisAction;
+
+export const receiveProjectActivity = (
+    project: string,
+    analyses: Array<Analysis>,
+    paging: Paging
+): ReceiveProjectActivityAction => ({
+  type: 'RECEIVE_PROJECT_ACTIVITY',
+  project,
+  analyses,
+  paging
+});
+
+export const addEvent = (analysis: string, event: Event): AddEventAction => ({
+  type: 'ADD_PROJECT_ACTIVITY_EVENT',
+  analysis,
+  event
+});
+
+export const deleteEvent = (analysis: string, event: string): DeleteEventAction => ({
+  type: 'DELETE_PROJECT_ACTIVITY_EVENT',
+  analysis,
+  event
+});
+
+export const changeEvent = (event: string, changes: Object): ChangeEventAction => ({
+  type: 'CHANGE_PROJECT_ACTIVITY_EVENT',
+  event,
+  changes
+});
+
+export const deleteAnalysis = (project: string, analysis: string): DeleteAnalysisAction => ({
+  type: 'DELETE_PROJECT_ACTIVITY_ANALYSIS',
+  project,
+  analysis
+});
+
+type State = {
+  analyses: AnalysesState,
+  analysesByProject: AnalysesByProjectState,
+  events: EventsState,
+  filter: string,
+  paging: PagingState,
+};
+
+export default combineReducers({ analyses, analysesByProject, events, paging });
+
+const getEvent = (state: State, key: string): Event => (
+    fromEvents.getEvent(state.events, key)
+);
+
+const getAnalysis = (state: State, key: string) => {
+  const analysis = fromAnalyses.getAnalysis(state.analyses, key);
+  const events: Array<Event> = analysis.events.map(key => getEvent(state, key));
+  return { ...analysis, events };
+};
+
+export const getAnalyses = (state: State, project: string) => (
+    state.analysesByProject[project] && state.analysesByProject[project].map(key => getAnalysis(state, key))
+);
+export const getPaging = (state: State, project: string) => (
+    state.paging[project]
+);
diff --git a/server/sonar-web/src/main/js/store/projectActivity/events.js b/server/sonar-web/src/main/js/store/projectActivity/events.js
new file mode 100644 (file)
index 0000000..88e6fc2
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import keyBy from 'lodash/keyBy';
+import type {
+  Action,
+  ReceiveProjectActivityAction,
+  AddEventAction,
+  DeleteEventAction,
+  ChangeEventAction
+} from './duck';
+
+export type State = {
+  [key: string]: {
+    key: string,
+    name: string;
+    category: string;
+    description?: string;
+  }
+};
+
+const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => {
+  const events = {};
+  action.analyses.forEach(analysis => {
+    Object.assign(events, keyBy(analysis.events, 'key'));
+  });
+  return { ...state, ...events };
+};
+
+const addEvent = (state: State, action: AddEventAction): State => {
+  return { ...state, [action.event.key]: action.event };
+};
+
+const deleteEvent = (state: State, action: DeleteEventAction): State => {
+  const newState = { ...state };
+  delete newState[action.event];
+  return newState;
+};
+
+const changeEvent = (state: State, action: ChangeEventAction): State => {
+  const newEvent = { ...state[action.event], ...action.changes };
+  return { ...state, [action.event]: newEvent };
+};
+
+export default (state: State = {}, action: Action): State => {
+  switch (action.type) {
+    case 'RECEIVE_PROJECT_ACTIVITY':
+      return receiveProjectActivity(state, action);
+    case 'ADD_PROJECT_ACTIVITY_EVENT':
+      return addEvent(state, action);
+    case 'DELETE_PROJECT_ACTIVITY_EVENT':
+      return deleteEvent(state, action);
+    case 'CHANGE_PROJECT_ACTIVITY_EVENT':
+      return changeEvent(state, action);
+    default:
+      return state;
+  }
+};
+
+export const getEvent = (state: State, key: string) => (
+    state[key]
+);
diff --git a/server/sonar-web/src/main/js/store/projectActivity/paging.js b/server/sonar-web/src/main/js/store/projectActivity/paging.js
new file mode 100644 (file)
index 0000000..46287a1
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+// @flow
+import type { Paging, ReceiveProjectActivityAction } from './duck';
+
+export type State = {
+  [key: string]: Paging
+};
+
+export default (state: State = {}, action: ReceiveProjectActivityAction): State => {
+  if (action.type === 'RECEIVE_PROJECT_ACTIVITY') {
+    return { ...state, [action.project]: action.paging };
+  }
+
+  return state;
+};
+
index 005c1158025c52ed179192d0067441fb9b4110fd..c6408588568b86be5b64989efcae3ae3f4a75b09 100644 (file)
@@ -26,7 +26,7 @@ import { addGlobalErrorMessage } from './globalMessages/duck';
 import { parseError } from '../apps/code/utils';
 import { setAppState } from './appState/duck';
 
-const onFail = dispatch => error => (
+export const onFail = dispatch => error => (
     parseError(error).then(message => dispatch(addGlobalErrorMessage(message)))
 );
 
index 1a9e879336996794240370b198dd655d5fa16650..c7d223c78bf1e67cda70891b126d6a4a569aae27 100644 (file)
@@ -25,6 +25,7 @@ import favorites, * as fromFavorites from './favorites/duck';
 import languages, * as fromLanguages from './languages/reducer';
 import measures, * as fromMeasures from './measures/reducer';
 import globalMessages, * as fromGlobalMessages from './globalMessages/duck';
+import projectActivity from './projectActivity/duck';
 
 import measuresApp, * as fromMeasuresApp from '../apps/component-measures/store/rootReducer';
 import permissionsApp, * as fromPermissionsApp from '../apps/permissions/shared/store/rootReducer';
@@ -40,6 +41,7 @@ export default combineReducers({
   favorites,
   languages,
   measures,
+  projectActivity,
   users,
 
   // apps
@@ -83,6 +85,10 @@ export const getComponentMeasures = (state, componentKey) => (
     fromMeasures.getComponentMeasures(state.measures, componentKey)
 );
 
+export const getProjectActivity = state => (
+    state.projectActivity
+);
+
 export const getProjects = state => (
     fromProjectsApp.getProjects(state.projectsApp)
 );
index 97397d836cd10314411c16add84cee63c9c03b55..361edd2dc182b5c6ff186d51515472a53ade88b6 100644 (file)
@@ -38,3 +38,7 @@ const finalCreateStore = compose(
 export default function configureStore (rootReducer, initialState) {
   return finalCreateStore(rootReducer, initialState);
 }
+
+export const configureTestStore = (rootReducer, initialState) => (
+    createStore(rootReducer, initialState)
+);
index 241364a6066d6804f906c120bbf266ad4756e154..bc771a5d25b3919ff63821316e63fefef989e58e 100644 (file)
@@ -20,7 +20,8 @@
 @import (reference) "../mixins";
 @import (reference) "../variables";
 
-.modal {
+.modal,
+.ReactModal__Content {
   position: fixed;
   z-index: @modal-z-index;
   top: 0;
@@ -32,7 +33,8 @@
   transition: all 0.2s ease;
 }
 
-.modal.in {
+.modal.in,
+.ReactModal__Content--after-open {
   top: 15%;
   opacity: 1;
 }
   margin-left: -45vw;
 }
 
-.modal-overlay {
+.modal-overlay,
+.ReactModal__Overlay {
   position: fixed;
   z-index: @modal-overlay-z-index;
-  top: 0; bottom: 0; left: 0; right: 0;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
   background-color: rgba(0, 0, 0, 0.7);
   opacity: 0;
   transition: all 0.2s ease;
 }
 
-.modal-overlay.in {
+.modal-overlay.in,
+.ReactModal__Overlay--after-open {
   opacity: 1;
 }
 
-.modal-open {
+.modal-open,
+.ReactModal__Body--open {
   overflow: hidden;
 }
 
@@ -69,8 +77,8 @@
 
 .modal-head {
   padding: 0 10px;
-  background-color: #EFEFEF;
-  border-bottom: 1px solid #DDD;
+  background-color: #efefef;
+  border-bottom: 1px solid #ddd;
 }
 
 .modal-head h1, .modal-head h2 {
@@ -167,9 +175,9 @@ ul.modal-head-metadata li {
 .modal-foot {
   text-align: right;
   padding: 8px 10px;
-  border-top: 1px solid #CCC;
+  border-top: 1px solid #ccc;
   line-height: 30px;
-  background-color: #EFEFEF;
+  background-color: #efefef;
 
   button,
   .button,
index 4de11a1c3e873a5b96baac60eabea4ae56ce8efe..43748629980dff1b83a953e1e534e6a7cac0d8d2 100644 (file)
@@ -152,7 +152,6 @@ input[type="submit"].button-success {
 .button-clean,
 .button-clean:hover,
 .button-clean:focus {
-  margin: 0;
   padding: 0;
   line-height: 1;
   border: none;
@@ -161,6 +160,14 @@ input[type="submit"].button-success {
   color: @baseFontColor;
 }
 
+.button-clean path {
+  transition: opacity 0.3s ease;
+}
+
+.button-clean:hover path {
+  opacity: 0.8;
+}
+
 .button-link {
   display: inline;
   height: auto;
@@ -189,6 +196,15 @@ input[type="submit"].button-success {
   }
 }
 
+.button-small {
+  height: 20px;
+  line-height: 18px;
+
+  & > svg {
+    margin-top: 2px;
+  }
+}
+
 .button-group {
   display: inline-block;
   vertical-align: middle;
index fbe676ea9aab8372c5fd54caf67ae620ee2c4ee5..2ba39576c43eae974d69248b5b50c47d2f771ee7 100644 (file)
@@ -1,5 +1,11 @@
 # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
 # yarn lockfile v1
+
+
+Base64@~0.2.0:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/Base64/-/Base64-0.2.1.tgz#ba3a4230708e186705065e66babdd4c35cf60028"
+
 abab@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d"
@@ -187,14 +193,14 @@ async-each@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
 
+async@1.x, async@^1.3.0, async@^1.4.0, async@^1.4.2, async@^1.5.0:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
 async@^0.9.0:
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
 
-async@^1.3.0, async@^1.4.0, async@^1.4.2, async@^1.5.0, async@1.x:
-  version "1.5.2"
-  resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-
 async@~0.2.10, async@~0.2.6:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
@@ -203,18 +209,7 @@ asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
 
-autoprefixer@^6.3.1:
-  version "6.5.3"
-  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.5.3.tgz#2d853af66d04449fcf50db3066279ab54c3e4b01"
-  dependencies:
-    browserslist "~1.4.0"
-    caniuse-db "^1.0.30000578"
-    normalize-range "^0.1.2"
-    num2fraction "^1.2.2"
-    postcss "^5.2.5"
-    postcss-value-parser "^3.2.3"
-
-autoprefixer@6.4.1:
+autoprefixer@6.4.1, autoprefixer@^6.3.1:
   version "6.4.1"
   resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.4.1.tgz#c1ba8461759faf5fb7737dbdbe5ea46a78c68a6f"
   dependencies:
@@ -241,31 +236,7 @@ babel-code-frame@^6.20.0, babel-code-frame@^6.8.0:
     esutils "^2.0.2"
     js-tokens "^2.0.0"
 
-babel-core@^6.0.0, babel-core@^6.11.4, babel-core@^6.14.0, babel-core@^6.18.0:
-  version "6.20.0"
-  resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.20.0.tgz#ab0d7176d9dea434e66badadaf92237865eab1ec"
-  dependencies:
-    babel-code-frame "^6.20.0"
-    babel-generator "^6.20.0"
-    babel-helpers "^6.16.0"
-    babel-messages "^6.8.0"
-    babel-register "^6.18.0"
-    babel-runtime "^6.20.0"
-    babel-template "^6.16.0"
-    babel-traverse "^6.20.0"
-    babel-types "^6.20.0"
-    babylon "^6.11.0"
-    convert-source-map "^1.1.0"
-    debug "^2.1.1"
-    json5 "^0.5.0"
-    lodash "^4.2.0"
-    minimatch "^3.0.2"
-    path-is-absolute "^1.0.0"
-    private "^0.1.6"
-    slash "^1.0.0"
-    source-map "^0.5.0"
-
-babel-core@6.14.0:
+babel-core@6.14.0, babel-core@^6.0.0:
   version "6.14.0"
   resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.14.0.tgz#c9e13ed4e2f97329215496fd9fb48f2b3bcb9b42"
   dependencies:
@@ -291,6 +262,30 @@ babel-core@6.14.0:
     slash "^1.0.0"
     source-map "^0.5.0"
 
+babel-core@^6.11.4, babel-core@^6.14.0, babel-core@^6.18.0:
+  version "6.20.0"
+  resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.20.0.tgz#ab0d7176d9dea434e66badadaf92237865eab1ec"
+  dependencies:
+    babel-code-frame "^6.20.0"
+    babel-generator "^6.20.0"
+    babel-helpers "^6.16.0"
+    babel-messages "^6.8.0"
+    babel-register "^6.18.0"
+    babel-runtime "^6.20.0"
+    babel-template "^6.16.0"
+    babel-traverse "^6.20.0"
+    babel-types "^6.20.0"
+    babylon "^6.11.0"
+    convert-source-map "^1.1.0"
+    debug "^2.1.1"
+    json5 "^0.5.0"
+    lodash "^4.2.0"
+    minimatch "^3.0.2"
+    path-is-absolute "^1.0.0"
+    private "^0.1.6"
+    slash "^1.0.0"
+    source-map "^0.5.0"
+
 babel-eslint@6.1.2:
   version "6.1.2"
   resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-6.1.2.tgz#5293419fe3672d66598d327da9694567ba6a5f2f"
@@ -423,7 +418,7 @@ babel-helpers@^6.16.0, babel-helpers@^6.8.0:
     babel-runtime "^6.0.0"
     babel-template "^6.16.0"
 
-babel-jest@^15.0.0, babel-jest@15.0.0:
+babel-jest@15.0.0, babel-jest@^15.0.0:
   version "15.0.0"
   resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-15.0.0.tgz#6a9e2e3999f241383db9ab1e2ef6704401d74242"
   dependencies:
@@ -711,14 +706,14 @@ babel-plugin-transform-react-display-name@^6.3.13:
   dependencies:
     babel-runtime "^6.0.0"
 
-babel-plugin-transform-react-jsx-self@^6.11.0, babel-plugin-transform-react-jsx-self@6.11.0:
+babel-plugin-transform-react-jsx-self@6.11.0, babel-plugin-transform-react-jsx-self@^6.11.0:
   version "6.11.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.11.0.tgz#605c9450c1429f97a930f7e1dfe3f0d9d0dbd0f4"
   dependencies:
     babel-plugin-syntax-jsx "^6.8.0"
     babel-runtime "^6.9.0"
 
-babel-plugin-transform-react-jsx-source@^6.3.13, babel-plugin-transform-react-jsx-source@6.9.0:
+babel-plugin-transform-react-jsx-source@6.9.0, babel-plugin-transform-react-jsx-source@^6.3.13:
   version "6.9.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.9.0.tgz#af684a05c2067a86e0957d4f343295ccf5dccf00"
   dependencies:
@@ -733,12 +728,6 @@ babel-plugin-transform-react-jsx@^6.3.13:
     babel-plugin-syntax-jsx "^6.8.0"
     babel-runtime "^6.0.0"
 
-babel-plugin-transform-regenerator@^6.16.0:
-  version "6.20.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.20.0.tgz#a546cd2aa1c9889929d5c427a31303847847ab75"
-  dependencies:
-    regenerator-transform "0.9.8"
-
 babel-plugin-transform-regenerator@6.14.0:
   version "6.14.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.14.0.tgz#119119b20c8b4283f6c77f0170d404c3c654bec8"
@@ -753,6 +742,12 @@ babel-plugin-transform-regenerator@6.14.0:
     babylon "^6.9.0"
     private "~0.1.5"
 
+babel-plugin-transform-regenerator@^6.16.0:
+  version "6.20.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.20.0.tgz#a546cd2aa1c9889929d5c427a31303847847ab75"
+  dependencies:
+    regenerator-transform "0.9.8"
+
 babel-plugin-transform-runtime@6.15.0:
   version "6.15.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.15.0.tgz#3d75b4d949ad81af157570273846fb59aeb0d57c"
@@ -873,6 +868,13 @@ babel-register@^6.14.0, babel-register@^6.18.0:
     mkdirp "^0.5.1"
     source-map-support "^0.4.2"
 
+babel-runtime@6.11.6:
+  version "6.11.6"
+  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.11.6.tgz#6db707fef2d49c49bfa3cb64efdb436b518b8222"
+  dependencies:
+    core-js "^2.4.0"
+    regenerator-runtime "^0.9.5"
+
 babel-runtime@^5.0.0:
   version "5.8.38"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-5.8.38.tgz#1c0b02eb63312f5f087ff20450827b425c9d4c19"
@@ -886,13 +888,6 @@ babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtim
     core-js "^2.4.0"
     regenerator-runtime "^0.10.0"
 
-babel-runtime@6.11.6:
-  version "6.11.6"
-  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.11.6.tgz#6db707fef2d49c49bfa3cb64efdb436b518b8222"
-  dependencies:
-    core-js "^2.4.0"
-    regenerator-runtime "^0.9.5"
-
 babel-template@^6.14.0, babel-template@^6.15.0, babel-template@^6.16.0, babel-template@^6.8.0:
   version "6.16.0"
   resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.16.0.tgz#e149dd1a9f03a35f817ddbc4d0481988e7ebc8ca"
@@ -953,13 +948,7 @@ backbone.wreqr@^1.0.0:
     backbone ">=0.9.9 <=1.3.x"
     underscore ">=1.3.3 <=1.8.3"
 
-"backbone@>=0.9.9 <=1.3.x":
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.3.3.tgz#4cc80ea7cb1631ac474889ce40f2f8bc683b2999"
-  dependencies:
-    underscore ">=1.8.3"
-
-"backbone@1.0.0 - 1.2.3", backbone@1.2.3:
+"backbone@1.0.0 - 1.2.3", backbone@1.2.3, "backbone@>=0.9.9 <=1.3.x":
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.2.3.tgz#c22cfd07fc86ebbeae61d18929ed115e999d65b9"
   dependencies:
@@ -973,10 +962,6 @@ base64-js@^1.0.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
 
-Base64@~0.2.0:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/Base64/-/Base64-0.2.1.tgz#ba3a4230708e186705065e66babdd4c35cf60028"
-
 batch@0.5.3:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464"
@@ -1052,12 +1037,6 @@ browserslist@~1.3.6:
   dependencies:
     caniuse-db "^1.0.30000525"
 
-browserslist@~1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.4.0.tgz#9cfdcf5384d9158f5b70da2aa00b30e8ff019049"
-  dependencies:
-    caniuse-db "^1.0.30000539"
-
 bser@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169"
@@ -1117,7 +1096,7 @@ camelcase@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
 
-caniuse-db@^1.0.30000525, caniuse-db@^1.0.30000527, caniuse-db@^1.0.30000539, caniuse-db@^1.0.30000578:
+caniuse-db@^1.0.30000525, caniuse-db@^1.0.30000527:
   version "1.0.30000597"
   resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000597.tgz#b52e6cbe9dc83669affb98501629feaee1af6588"
 
@@ -1143,7 +1122,7 @@ center-align@^0.1.1:
     align-text "^0.1.3"
     lazy-cache "^1.0.3"
 
-chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3, chalk@1.1.3:
+chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -1190,14 +1169,14 @@ clap@^1.0.9:
   dependencies:
     chalk "^1.1.3"
 
-classnames@^2.2.3:
-  version "2.2.5"
-  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
-
 classnames@2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.0.tgz#8f61df81f356c45d18a31d83fde4dfb194ea8722"
 
+classnames@^2.2.3:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
+
 clean-css@3.4.x:
   version "3.4.21"
   resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.21.tgz#2101d5dbd19d63dbc16a75ebd570e7c33948f65b"
@@ -1302,29 +1281,29 @@ colormin@^1.0.5:
     css-color-names "0.0.4"
     has "^1.0.1"
 
-colors@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
-
 colors@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
+colors@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
+
 combined-stream@^1.0.5, combined-stream@~1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@^2.9.0, commander@2.9.x:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+commander@2.8.x, commander@~2.8.1:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
   dependencies:
     graceful-readlink ">= 1.0.0"
 
-commander@~2.8.1, commander@2.8.x:
-  version "2.8.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
+commander@2.9.x, commander@^2.9.0:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
   dependencies:
     graceful-readlink ">= 1.0.0"
 
@@ -1361,7 +1340,7 @@ concat-stream@^1.4.6:
     readable-stream "~2.0.0"
     typedarray "~0.0.5"
 
-connect-history-api-fallback@^1.3.0, connect-history-api-fallback@1.3.0:
+connect-history-api-fallback@1.3.0, connect-history-api-fallback@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
 
@@ -1391,7 +1370,7 @@ content-type-parser@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94"
 
-content-type@~1.0.1, content-type@~1.0.2:
+content-type@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
 
@@ -1407,10 +1386,6 @@ cookie@0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.1.5.tgz#6ab9948a4b1ae21952cd2588530a4722d4044d7c"
 
-cookie@0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
-
 core-js@^1.0.0, core-js@^1.0.1:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@@ -1430,16 +1405,16 @@ cross-env@2.0.0:
     cross-spawn "^3.0.1"
     lodash.assign "^3.2.0"
 
-cross-spawn@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+cross-spawn@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.0.tgz#8254774ab4786b8c5b3cf4dfba66ce563932c252"
   dependencies:
     lru-cache "^4.0.1"
     which "^1.2.9"
 
-cross-spawn@4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.0.tgz#8254774ab4786b8c5b3cf4dfba66ce563932c252"
+cross-spawn@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
   dependencies:
     lru-cache "^4.0.1"
     which "^1.2.9"
@@ -1554,7 +1529,7 @@ csso@~2.2.1:
     clap "^1.0.9"
     source-map "^0.5.3"
 
-"cssom@>= 0.3.0 < 0.4.0", cssom@0.3.x:
+cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0":
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.1.tgz#c9e37ef2490e64f6d1baa10fda852257082c25d3"
 
@@ -1564,16 +1539,16 @@ csso@~2.2.1:
   dependencies:
     cssom "0.3.x"
 
+d3@3.5.6:
+  version "3.5.6"
+  resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.6.tgz#9451c651ca733fb9672c81fb7f2655164a73a42d"
+
 d@^0.1.1, d@~0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
   dependencies:
     es5-ext "~0.10.2"
 
-d3@3.5.6:
-  version "3.5.6"
-  resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.6.tgz#9451c651ca733fb9672c81fb7f2655164a73a42d"
-
 damerau-levenshtein@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.3.tgz#ae4f4ce0b62acae10ff63a01bb08f652f5213af2"
@@ -1604,7 +1579,7 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
 
-deep-equal@^1.0.0, deep-equal@^1.0.1, deep-equal@1.0.1:
+deep-equal@1.0.1, deep-equal@^1.0.0, deep-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
 
@@ -1675,16 +1650,16 @@ diff@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.1.0.tgz#9406c73a401e6c2b3ba901c5e2c44eb6a60c5385"
 
-doctrine@^1.2.2:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+doctrine@1.2.x:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.2.3.tgz#6aec6bbd62cf89dd498cae70c0ed9f49da873a6a"
   dependencies:
     esutils "^2.0.2"
     isarray "^1.0.0"
 
-doctrine@1.2.x:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.2.3.tgz#6aec6bbd62cf89dd498cae70c0ed9f49da873a6a"
+doctrine@^1.2.2:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
   dependencies:
     esutils "^2.0.2"
     isarray "^1.0.0"
@@ -1699,7 +1674,7 @@ dom-helpers@^2.4.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-2.4.0.tgz#9bb4b245f637367b1fa670274272aa28fe06c367"
 
-dom-serializer@~0.1.0, dom-serializer@0:
+dom-serializer@0, dom-serializer@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
   dependencies:
@@ -1714,14 +1689,10 @@ domain-browser@^1.1.1:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
 
-domelementtype@~1.1.1:
+domelementtype@1, domelementtype@~1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
 
-domelementtype@1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
-
 domhandler@2.1:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594"
@@ -1765,14 +1736,14 @@ ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
 
+element-class@^0.2.0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e"
+
 emojis-list@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
 
-encodeurl@~1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
-
 encoding@^0.1.11:
   version "0.1.12"
   resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
@@ -1787,14 +1758,14 @@ enhanced-resolve@~0.9.0:
     memory-fs "^0.2.0"
     tapable "^0.1.8"
 
-entities@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
-
 entities@1.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26"
 
+entities@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+
 enzyme@2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-2.2.0.tgz#972f1b89d771356ad4e06a15b4736173ec64aed5"
@@ -1805,7 +1776,7 @@ enzyme@2.2.0:
     object.assign "^4.0.3"
     object.values "^1.0.3"
 
-errno@^0.1.1, errno@^0.1.3, "errno@>=0.1.1 <0.2.0-0":
+"errno@>=0.1.1 <0.2.0-0", errno@^0.1.1, errno@^0.1.3:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
   dependencies:
@@ -1870,7 +1841,7 @@ es6-set@^0.1.4, es6-set@~0.1.3:
     es6-symbol "3"
     event-emitter "~0.3.4"
 
-es6-symbol@~3.1, es6-symbol@~3.1.0, es6-symbol@3:
+es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa"
   dependencies:
@@ -1886,15 +1857,15 @@ es6-weak-map@^2.0.1:
     es6-iterator "2"
     es6-symbol "3"
 
-escape-html@~1.0.3, escape-html@1.0.3:
+escape-html@1.0.3, escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
 
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@1.0.5:
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 
-escodegen@^1.6.1, escodegen@1.8.x:
+escodegen@1.8.x, escodegen@^1.6.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018"
   dependencies:
@@ -2019,7 +1990,7 @@ espree@^3.1.6:
     acorn "^4.0.1"
     acorn-jsx "^3.0.0"
 
-esprima@^2.6.0, esprima@^2.7.1, esprima@2.7.x:
+esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1:
   version "2.7.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
 
@@ -2069,7 +2040,7 @@ events@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
 
-eventsource@^0.1.3, eventsource@~0.1.6:
+eventsource@^0.1.3:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232"
   dependencies:
@@ -2081,6 +2052,10 @@ exec-sh@^0.2.0:
   dependencies:
     merge "^1.1.3"
 
+exenv@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.0.tgz#3835f127abf075bfe082d0aed4484057c78e3c89"
+
 exit-hook@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -2108,38 +2083,7 @@ express-http-proxy@0.6.0:
     raw-body "^1.1.6"
     type-is "^1.2.0"
 
-express@^4.13.3:
-  version "4.14.0"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.14.0.tgz#c1ee3f42cdc891fb3dc650a8922d51ec847d0d66"
-  dependencies:
-    accepts "~1.3.3"
-    array-flatten "1.1.1"
-    content-disposition "0.5.1"
-    content-type "~1.0.2"
-    cookie "0.3.1"
-    cookie-signature "1.0.6"
-    debug "~2.2.0"
-    depd "~1.1.0"
-    encodeurl "~1.0.1"
-    escape-html "~1.0.3"
-    etag "~1.7.0"
-    finalhandler "0.5.0"
-    fresh "0.3.0"
-    merge-descriptors "1.0.1"
-    methods "~1.1.2"
-    on-finished "~2.3.0"
-    parseurl "~1.3.1"
-    path-to-regexp "0.1.7"
-    proxy-addr "~1.1.2"
-    qs "6.2.0"
-    range-parser "~1.2.0"
-    send "0.14.1"
-    serve-static "~1.11.1"
-    type-is "~1.6.13"
-    utils-merge "1.0.0"
-    vary "~1.1.0"
-
-express@4.13.4:
+express@4.13.4, express@^4.13.3:
   version "4.13.4"
   resolved "https://registry.yarnpkg.com/express/-/express-4.13.4.tgz#3c0b76f3c77590c8345739061ec0bd3ba067ec24"
   dependencies:
@@ -2205,12 +2149,6 @@ faye-websocket@^0.10.0:
   dependencies:
     websocket-driver ">=0.5.1"
 
-faye-websocket@~0.11.0:
-  version "0.11.0"
-  resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.0.tgz#d9ccf0e789e7db725d74bc4877d23aa42972ac50"
-  dependencies:
-    websocket-driver ">=0.5.1"
-
 faye-websocket@~0.7.3:
   version "0.7.3"
   resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.7.3.tgz#cc4074c7f4a4dfd03af54dd65c354b135132ce11"
@@ -2223,6 +2161,14 @@ fb-watchman@^1.8.0, fb-watchman@^1.9.0:
   dependencies:
     bser "^1.0.2"
 
+fbjs@0.1.0-alpha.10:
+  version "0.1.0-alpha.10"
+  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.1.0-alpha.10.tgz#46e457c09cbefb51fc752a3e030e7b67fcc384c8"
+  dependencies:
+    core-js "^1.0.0"
+    promise "^7.0.3"
+    whatwg-fetch "^0.9.0"
+
 fbjs@^0.8.4:
   version "0.8.6"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.6.tgz#7eb67d6986b2d5007a9b6e92e0e7cb6f75cad290"
@@ -2234,14 +2180,6 @@ fbjs@^0.8.4:
     promise "^7.1.1"
     ua-parser-js "^0.7.9"
 
-fbjs@0.1.0-alpha.10:
-  version "0.1.0-alpha.10"
-  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.1.0-alpha.10.tgz#46e457c09cbefb51fc752a3e030e7b67fcc384c8"
-  dependencies:
-    core-js "^1.0.0"
-    promise "^7.0.3"
-    whatwg-fetch "^0.9.0"
-
 figures@^1.3.5:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
@@ -2296,17 +2234,7 @@ finalhandler@0.4.1:
     on-finished "~2.3.0"
     unpipe "~1.0.0"
 
-finalhandler@0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7"
-  dependencies:
-    debug "~2.2.0"
-    escape-html "~1.0.3"
-    on-finished "~2.3.0"
-    statuses "~1.3.0"
-    unpipe "~1.0.0"
-
-find-cache-dir@^0.1.1, find-cache-dir@0.1.1:
+find-cache-dir@0.1.1, find-cache-dir@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
   dependencies:
@@ -2461,7 +2389,7 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
-glob@^5.0.15, glob@5.x:
+glob@5.x, glob@^5.0.15:
   version "5.0.15"
   resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
   dependencies:
@@ -2536,6 +2464,14 @@ handlebars-loader@1.1.4:
     fastparse "^1.0.0"
     loader-utils "0.2.x"
 
+handlebars@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-2.0.0.tgz#6e9d7f8514a3467fa5e9f82cc158ecfc1d5ac76f"
+  dependencies:
+    optimist "~0.3"
+  optionalDependencies:
+    uglify-js "~2.3"
+
 handlebars@^4.0.1, handlebars@^4.0.3:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7"
@@ -2546,14 +2482,6 @@ handlebars@^4.0.1, handlebars@^4.0.3:
   optionalDependencies:
     uglify-js "^2.6"
 
-handlebars@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-2.0.0.tgz#6e9d7f8514a3467fa5e9f82cc158ecfc1d5ac76f"
-  dependencies:
-    optimist "~0.3"
-  optionalDependencies:
-    uglify-js "~2.3"
-
 har-validator@~2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
@@ -2596,18 +2524,18 @@ he@1.1.x:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.1.0.tgz#29319d49beec13a9b1f3c4f9b2a6dde4859bb2a7"
 
-history@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/history/-/history-2.1.2.tgz#4aa2de897a0e4867e4539843be6ecdb2986bfdec"
+history@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/history/-/history-2.0.0.tgz#6d5144af2da8a3dea4e5f1abae11a3c2e868e2c7"
   dependencies:
     deep-equal "^1.0.0"
     invariant "^2.0.0"
     query-string "^3.0.0"
     warning "^2.0.0"
 
-history@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/history/-/history-2.0.0.tgz#6d5144af2da8a3dea4e5f1abae11a3c2e868e2c7"
+history@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/history/-/history-2.1.2.tgz#4aa2de897a0e4867e4539843be6ecdb2986bfdec"
   dependencies:
     deep-equal "^1.0.0"
     invariant "^2.0.0"
@@ -2740,10 +2668,6 @@ https-browserify@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.0.tgz#b3ffdfe734b2a3d4a9efd58e8654c91fce86eafd"
 
-iconv-lite@^0.4.13, iconv-lite@~0.4.13:
-  version "0.4.15"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
-
 iconv-lite@0.4.13:
   version "0.4.13"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
@@ -2752,6 +2676,10 @@ iconv-lite@0.4.8:
   version "0.4.8"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.8.tgz#c6019a7595f2cefca702eab694a010bcd9298d20"
 
+iconv-lite@^0.4.13, iconv-lite@~0.4.13:
+  version "0.4.15"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
+
 icss-replace-symbols@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz#cb0b6054eb3af6edc9ab1d62d01933e2d4c8bfa5"
@@ -2794,7 +2722,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1, inherits@2, inherits@2.0.3:
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
 
@@ -2842,10 +2770,6 @@ ipaddr.js@1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.0.5.tgz#5fa78cf301b825c78abc3042d812723049ea23c7"
 
-ipaddr.js@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.1.1.tgz#c791d95f52b29c1247d5df80ada39b8a73647230"
-
 is-absolute-url@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@@ -3011,14 +2935,14 @@ is-utf8@^0.2.0:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
 
-isarray@^1.0.0, isarray@~1.0.0, isarray@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
-
 isarray@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
 
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
 isexe@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
@@ -3321,7 +3245,7 @@ js-tokens@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5"
 
-js-yaml@^3.5.1, js-yaml@3.x:
+js-yaml@3.x, js-yaml@^3.5.1:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
   dependencies:
@@ -3508,7 +3432,7 @@ load-json-file@^1.0.0:
     pinkie-promise "^2.0.0"
     strip-bom "^2.0.0"
 
-loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.3, loader-utils@^0.2.5, loader-utils@^0.2.7, loader-utils@~0.2.2, loader-utils@~0.2.5, loader-utils@0.2.x:
+loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.3, loader-utils@^0.2.5, loader-utils@^0.2.7, loader-utils@~0.2.2, loader-utils@~0.2.5:
   version "0.2.16"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
   dependencies:
@@ -3667,14 +3591,14 @@ lodash.words@^3.0.0:
   dependencies:
     lodash._root "^3.0.0"
 
-lodash@^4.0.0, lodash@^4.1.0, lodash@^4.15.0, lodash@^4.16.4, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1:
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42"
-
-lodash@4.6.1:
+lodash@4.6.1, lodash@^4.0.0, lodash@^4.2.0:
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.6.1.tgz#df00c1164ad236b183cfc3887a5e8d38cc63cbbc"
 
+lodash@^4.1.0, lodash@^4.15.0, lodash@^4.16.4, lodash@^4.17.2, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1:
+  version "4.17.2"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42"
+
 longest@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -3781,7 +3705,7 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.6, m
   dependencies:
     mime-db "~1.25.0"
 
-mime@^1.2.11, mime@^1.3.4, mime@1.3.4:
+mime@1.3.4, mime@^1.2.11, mime@^1.3.4:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
 
@@ -3791,7 +3715,7 @@ min-document@^2.19.0:
   dependencies:
     dom-walk "^0.1.0"
 
-minimatch@^3.0.0, minimatch@^3.0.2, "minimatch@2 || 3":
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
   dependencies:
@@ -3809,19 +3733,15 @@ minimatch@3.0.2:
   dependencies:
     brace-expansion "^1.0.0"
 
+minimist@0.0.8, minimist@~0.0.1:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
 minimist@^1.1.1, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
-minimist@~0.0.1:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
-
-minimist@0.0.8:
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-
-mkdirp@^0.5.0, mkdirp@^0.5.1, "mkdirp@>=0.5 0", mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@0.5.x:
+mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -3951,7 +3871,7 @@ node-pre-gyp@^0.6.29:
     tar "~2.2.1"
     tar-pack "~3.3.0"
 
-nopt@~3.0.6, nopt@3.x:
+nopt@3.x, nopt@~3.0.6:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
   dependencies:
@@ -4060,7 +3980,7 @@ on-headers@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
 
-once@^1.3.0, once@1.x:
+once@1.x, once@^1.3.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   dependencies:
@@ -4172,16 +4092,16 @@ path-browserify@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
 
-path-exists@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081"
-
-path-exists@^2.0.0, path-exists@2.1.0:
+path-exists@2.1.0, path-exists@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
   dependencies:
     pinkie-promise "^2.0.0"
 
+path-exists@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081"
+
 path-is-absolute@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@@ -4473,7 +4393,7 @@ postcss-zindex@^2.0.1:
     postcss "^5.0.4"
     uniqs "^2.0.0"
 
-postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.1.2, postcss@^5.2.5:
+postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.1.2:
   version "5.2.6"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.6.tgz#a252cd67cd52585035f17e9ad12b35137a7bdd9e"
   dependencies:
@@ -4538,13 +4458,6 @@ proxy-addr@~1.0.10:
     forwarded "~0.1.0"
     ipaddr.js "1.0.5"
 
-proxy-addr@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37"
-  dependencies:
-    forwarded "~0.1.0"
-    ipaddr.js "1.1.1"
-
 prr@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
@@ -4553,29 +4466,25 @@ pseudomap@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
 
-punycode@^1.2.4, punycode@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
-
 punycode@1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
 
+punycode@^1.2.4, punycode@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
 q@^1.1.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
 
-qs@~6.3.0:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
-
 qs@4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607"
 
-qs@6.2.0:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
+qs@~6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
 
 query-string@^3.0.0:
   version "3.0.3"
@@ -4609,11 +4518,7 @@ randomatic@^1.1.3:
     is-number "^2.0.2"
     kind-of "^3.0.2"
 
-range-parser@^1.0.3, range-parser@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
-
-range-parser@~1.0.3:
+range-parser@^1.0.3, range-parser@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.0.3.tgz#6872823535c692e2c2a0103826afd82c2e0ff175"
 
@@ -4679,6 +4584,14 @@ react-input-autosize@^0.6.10:
   version "0.6.13"
   resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-0.6.13.tgz#386ff7a9d2c3dc016c265bf2e59d397050f65af7"
 
+react-modal@^1.6.4:
+  version "1.6.4"
+  resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-1.6.4.tgz#639383931cbaae7946a92de9d5a7c05a4a1cab82"
+  dependencies:
+    element-class "^0.2.0"
+    exenv "1.2.0"
+    lodash.assign "^4.2.0"
+
 react-proxy@^1.1.7:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-1.1.8.tgz#9dbfd9d927528c3aa9f444e4558c37830ab8c26a"
@@ -4760,39 +4673,25 @@ read-pkg@^1.0.0:
     normalize-package-data "^2.3.2"
     path-type "^1.0.0"
 
-readable-stream@^1.0.27-1, readable-stream@^1.1.13:
-  version "1.1.14"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+readable-stream@1.0:
+  version "1.0.34"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
   dependencies:
     core-util-is "~1.0.0"
     inherits "~2.0.1"
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
-  dependencies:
-    buffer-shims "^1.0.0"
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "~1.0.0"
-    process-nextick-args "~1.0.6"
-    string_decoder "~0.10.x"
-    util-deprecate "~1.0.1"
-
-readable-stream@~2.0.0:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
+readable-stream@1.1, readable-stream@^1.0.27-1, readable-stream@^1.1.13:
+  version "1.1.14"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
   dependencies:
     core-util-is "~1.0.0"
     inherits "~2.0.1"
-    isarray "~1.0.0"
-    process-nextick-args "~1.0.6"
+    isarray "0.0.1"
     string_decoder "~0.10.x"
-    util-deprecate "~1.0.1"
 
-readable-stream@~2.1.4:
+"readable-stream@^2.0.0 || ^1.1.13", readable-stream@~2.1.4:
   version "2.1.5"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0"
   dependencies:
@@ -4804,23 +4703,16 @@ readable-stream@~2.1.4:
     string_decoder "~0.10.x"
     util-deprecate "~1.0.1"
 
-readable-stream@1.0:
-  version "1.0.34"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
-readable-stream@1.1:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@~2.0.0:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
   dependencies:
     core-util-is "~1.0.0"
     inherits "~2.0.1"
-    isarray "0.0.1"
+    isarray "~1.0.0"
+    process-nextick-args "~1.0.6"
     string_decoder "~0.10.x"
+    util-deprecate "~1.0.1"
 
 readdirp@^2.0.0:
   version "2.1.0"
@@ -5016,7 +4908,7 @@ resolve-from@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
 
-resolve@^1.1.6, resolve@1.1.7, resolve@1.1.x:
+resolve@1.1.7, resolve@1.1.x, resolve@^1.1.6:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
 
@@ -5033,7 +4925,7 @@ right-align@^0.1.1:
   dependencies:
     align-text "^0.1.1"
 
-rimraf@^2.2.8, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4, rimraf@2, rimraf@2.5.4:
+rimraf@2, rimraf@2.5.4, rimraf@^2.2.8, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4:
   version "2.5.4"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
   dependencies:
@@ -5078,7 +4970,7 @@ select@^1.0.6:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/select/-/select-1.1.0.tgz#a6c520cd9ab919ad81c7d1a273e0452f504dd7a2"
 
-semver@^5.1.0, semver@^5.3.0, semver@~5.3.0, "semver@2 || 3 || 4 || 5":
+"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@~5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
 
@@ -5116,24 +5008,6 @@ send@0.13.2:
     range-parser "~1.0.3"
     statuses "~1.2.1"
 
-send@0.14.1:
-  version "0.14.1"
-  resolved "https://registry.yarnpkg.com/send/-/send-0.14.1.tgz#a954984325392f51532a7760760e459598c89f7a"
-  dependencies:
-    debug "~2.2.0"
-    depd "~1.1.0"
-    destroy "~1.0.4"
-    encodeurl "~1.0.1"
-    escape-html "~1.0.3"
-    etag "~1.7.0"
-    fresh "0.3.0"
-    http-errors "~1.5.0"
-    mime "1.3.4"
-    ms "0.7.1"
-    on-finished "~2.3.0"
-    range-parser "~1.2.0"
-    statuses "~1.3.0"
-
 serve-index@^1.7.2:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.8.0.tgz#7c5d96c13fb131101f93c1c5774f8516a1e78d3b"
@@ -5154,15 +5028,6 @@ serve-static@~1.10.2:
     parseurl "~1.3.1"
     send "0.13.2"
 
-serve-static@~1.11.1:
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.1.tgz#d6cce7693505f733c759de57befc1af76c0f0805"
-  dependencies:
-    encodeurl "~1.0.1"
-    escape-html "~1.0.3"
-    parseurl "~1.3.1"
-    send "0.14.1"
-
 set-blocking@^2.0.0, set-blocking@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -5215,18 +5080,7 @@ sntp@1.x.x:
   dependencies:
     hoek "2.x.x"
 
-sockjs-client@^1.0.3:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.1.tgz#284843e9a9784d7c474b1571b3240fca9dda4bb0"
-  dependencies:
-    debug "^2.2.0"
-    eventsource "~0.1.6"
-    faye-websocket "~0.11.0"
-    inherits "^2.0.1"
-    json3 "^3.3.2"
-    url-parse "^1.1.1"
-
-sockjs-client@1.0.3:
+sockjs-client@1.0.3, sockjs-client@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.0.3.tgz#b0d8280998460eb2564c5d79d7e3d7cfd8a353ad"
   dependencies:
@@ -5260,7 +5114,13 @@ source-map-support@^0.4.2:
   dependencies:
     source-map "^0.5.3"
 
-source-map@^0.4.4, source-map@~0.4.1, source-map@0.4.x:
+source-map@0.1.x, source-map@~0.1.7:
+  version "0.1.43"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
+  dependencies:
+    amdefine ">=0.0.4"
+
+source-map@0.4.x, source-map@^0.4.4, source-map@~0.4.1:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
   dependencies:
@@ -5270,12 +5130,6 @@ source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, sour
   version "0.5.6"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
 
-source-map@~0.1.7, source-map@0.1.x:
-  version "0.1.43"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
-  dependencies:
-    amdefine ">=0.0.4"
-
 source-map@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
@@ -5315,14 +5169,14 @@ sshpk@^1.7.0:
     jsbn "~0.1.0"
     tweetnacl "~0.14.0"
 
-"statuses@>= 1.3.1 < 2", statuses@~1.3.0, statuses@1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
-
-statuses@~1.2.1:
+statuses@1, statuses@~1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.2.1.tgz#dded45cc18256d51ed40aec142489d5c61026d28"
 
+"statuses@>= 1.3.1 < 2":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+
 stream-browserify@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-1.0.0.tgz#bf9b4abfb42b274d751479e44e0ff2656b6f1193"
@@ -5338,10 +5192,6 @@ strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
 
-string_decoder@~0.10.25, string_decoder@~0.10.x:
-  version "0.10.31"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-
 string-width@^1.0.1, string-width@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -5361,11 +5211,15 @@ string.prototype.codepointat@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78"
 
+string_decoder@~0.10.25, string_decoder@~0.10.x:
+  version "0.10.31"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
 stringstream@~0.0.4:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
 
-strip-ansi@^3.0.0, strip-ansi@^3.0.1, strip-ansi@3.0.1:
+strip-ansi@3.0.1, strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
   dependencies:
@@ -5533,7 +5387,7 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
-type-is@^1.2.0, type-is@~1.6.13, type-is@~1.6.6:
+type-is@^1.2.0, type-is@~1.6.6:
   version "1.6.14"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2"
   dependencies:
@@ -5548,7 +5402,7 @@ ua-parser-js@^0.7.9:
   version "0.7.12"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb"
 
-uglify-js@^2.6, uglify-js@2.7.x:
+uglify-js@2.7.x, uglify-js@^2.6:
   version "2.7.5"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8"
   dependencies:
@@ -5582,7 +5436,7 @@ uid-number@~0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
 
-"underscore@>=1.3.3 <=1.8.3", "underscore@>=1.4.0 <=1.8.3", underscore@>=1.7.0, underscore@>=1.8.3, "underscore@1.4.4 - 1.8.3", underscore@1.8.3:
+"underscore@1.4.4 - 1.8.3", underscore@1.8.3, "underscore@>=1.3.3 <=1.8.3", "underscore@>=1.4.0 <=1.8.3", underscore@>=1.7.0:
   version "1.8.3"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
 
@@ -5608,16 +5462,16 @@ upper-case@^1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
 
-url-parse@^1.0.1, url-parse@^1.1.1:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
+url-parse@1.0.x:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
   dependencies:
     querystringify "0.0.x"
     requires-port "1.0.x"
 
-url-parse@1.0.x:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
+url-parse@^1.0.1:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
   dependencies:
     querystringify "0.0.x"
     requires-port "1.0.x"
@@ -5639,7 +5493,7 @@ util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
 
-util@~0.10.3, util@0.10.3:
+util@0.10.3, util@~0.10.3:
   version "0.10.3"
   resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
   dependencies:
@@ -5702,7 +5556,7 @@ walker@~1.0.5:
   dependencies:
     makeerror "1.0.x"
 
-warning@^2.0.0, warning@2.1.0:
+warning@2.1.0, warning@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/warning/-/warning-2.1.0.tgz#21220d9c63afc77a8c92111e011af705ce0c6901"
   dependencies:
@@ -5811,18 +5665,14 @@ whatwg-encoding@^1.0.1:
   dependencies:
     iconv-lite "0.4.13"
 
+whatwg-fetch@1.0.0, whatwg-fetch@>=0.10.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.0.0.tgz#01c2ac4df40e236aaa18480e3be74bd5c8eb798e"
+
 whatwg-fetch@^0.9.0:
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0"
 
-whatwg-fetch@>=0.10.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.1.tgz#078b9461bbe91cea73cbce8bb122a05f9e92b772"
-
-whatwg-fetch@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.0.0.tgz#01c2ac4df40e236aaa18480e3be74bd5c8eb798e"
-
 whatwg-url-compat@~0.6.5:
   version "0.6.5"
   resolved "https://registry.yarnpkg.com/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz#00898111af689bb097541cd5a45ca6c8798445bf"
@@ -5856,13 +5706,17 @@ wide-align@^1.1.0:
   dependencies:
     string-width "^1.0.1"
 
+window-size@0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
 window-size@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
 
-window-size@0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+wordwrap@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
 
 wordwrap@^1.0.0, wordwrap@~1.0.0:
   version "1.0.0"
@@ -5872,10 +5726,6 @@ wordwrap@~0.0.2:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
 
-wordwrap@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
-
 worker-farm@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.3.1.tgz#4333112bb49b17aa050b87895ca6b2cacf40e5ff"
@@ -5908,7 +5758,7 @@ xml-char-classes@^1.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
 
-xtend@^4.0.0, "xtend@>=4.0.0 <4.1.0-0":
+"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
 
@@ -5954,4 +5804,3 @@ yargs@~3.10.0:
     cliui "^2.1.0"
     decamelize "^1.0.0"
     window-size "0.1.0"
-
index 67b93b050ea40e09899716c1884d0fcb8de2180a..8ec7fda9c47c7ca2eab7646111b3b495df892664 100644 (file)
@@ -438,10 +438,10 @@ project_links.url=URL
 #------------------------------------------------------------------------------
 
 event.category.All=All
-event.category.Version=Version
-event.category.Alert=Quality Gate
-event.category.Profile=Quality Profile
-event.category.Other=Other
+event.category.VERSION=Version
+event.category.QUALITY_GATE=Quality Gate
+event.category.QUALITY_PROFILE=Quality Profile
+event.category.OTHER=Other
 
 
 #------------------------------------------------------------------------------
@@ -552,7 +552,8 @@ source.page=Source
 timemachine.page=Time Machine
 comparison.page=Compare
 view_projects.page=Projects
-
+project_activity.page=Activity
+project_activity.page.description=The page shows the history of project analyses.
 
 #------------------------------------------------------------------------------
 #
@@ -1150,10 +1151,22 @@ manual_rules.add_manual_rule=Add Manual Rule
 
 #------------------------------------------------------------------------------
 #
-# PROJECT HISTORY SERVICE
+# PROJECT ACTIVITY/HISTORY SERVICE
 #
 #------------------------------------------------------------------------------
 
+project_activity.project_analyzed=Project Analyzed
+project_activity.add_version=Create Version
+project_activity.remove_version=Remove Version
+project_activity.remove_version.question=Are you sure you want to delete this version?
+project_activity.change_version=Change Version
+project_activity.add_custom_event=Create Custom Event
+project_activity.change_custom_event=Change Event
+project_activity.remove_custom_event=Delete Event
+project_activity.remove_custom_event.question=Are you sure you want to delete this event?
+project_activity.delete_analysis=Delete Analysis
+project_activity.delete_analysis.question=Are you sure you want to delete this analysis from the project history?
+
 project_history.col.year=Year
 project_history.col.month=Month
 project_history.col.day=Day