@@ -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 |
@@ -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")) |
@@ -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 | |||
*/ |
@@ -17,7 +17,7 @@ | |||
* 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; | |||
package it.projectEvent; | |||
import com.sonar.orchestrator.Orchestrator; | |||
import com.sonar.orchestrator.build.SonarScanner; | |||
@@ -28,15 +28,12 @@ import org.junit.ClassRule; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import pageobjects.Navigation; | |||
import pageobjects.ProjectHistoryPage; | |||
import pageobjects.ProjectHistorySnapshotItem; | |||
import pageobjects.ProjectActivityPage; | |||
import pageobjects.ProjectAnalysisItem; | |||
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 { | |||
public class ProjectActivityPageTest { | |||
@ClassRule | |||
public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR; | |||
@@ -45,47 +42,54 @@ public class ProjectHistoryPageTest { | |||
public Navigation nav = Navigation.get(ORCHESTRATOR); | |||
@Before | |||
public void setUp() { | |||
public void setUp() throws Exception { | |||
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); | |||
analyzeProject("shared/xoo-history-v1", "2014-10-19"); | |||
analyzeProject("shared/xoo-history-v2", "2014-11-13"); | |||
List<ProjectHistorySnapshotItem> snapshots = page.getSnapshotsAsItems(); | |||
ProjectActivityPage page = openPage(); | |||
page.getAnalyses().shouldHaveSize(2); | |||
snapshots.get(0).getVersionText().shouldBe(text("1.0-SNAPSHOT")); | |||
snapshots.get(0).getDeleteButton().shouldNot(exist); | |||
List<ProjectAnalysisItem> analyses = page.getAnalysesAsItems(); | |||
analyses.get(0) | |||
.shouldHaveEventWithText("1.0-SNAPSHOT") | |||
.shouldNotHaveDeleteButton(); | |||
snapshots.get(1).getVersionText().shouldBe(text("0.9-SNAPSHOT")); | |||
snapshots.get(1).getDeleteButton().should(exist); | |||
analyses.get(1) | |||
.shouldHaveEventWithText("0.9-SNAPSHOT") | |||
.shouldHaveDeleteButton(); | |||
} | |||
@Test | |||
public void should_delete_snapshot() { | |||
ProjectHistoryPage page = openPage(); | |||
page.getSnapshots().shouldHaveSize(2); | |||
page.getSnapshotsAsItems().get(1).clickDelete(); | |||
confirm(); | |||
public void add_change_delete_custom_event() { | |||
analyzeProject(); | |||
openPage().getLastAnalysis() | |||
.addCustomEvent("foo") | |||
.changeLastEvent("bar") | |||
.deleteLastEvent(); | |||
} | |||
page.checkAlertDisplayed(); | |||
page.getSnapshots().shouldHaveSize(1); | |||
@Test | |||
public void delete_analysis() { | |||
analyzeProject(); | |||
analyzeProject(); | |||
openPage().getFirstAnalysis().delete(); | |||
} | |||
private ProjectHistoryPage openPage() { | |||
private ProjectActivityPage openPage() { | |||
nav.logIn().submitCredentials("admin", "admin"); | |||
return nav.openProjectHistory("sample"); | |||
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)); | |||
ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir(path)).setProperties("sonar.projectDate", date)); | |||
} | |||
} |
@@ -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()); |
@@ -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); | |||
} |
@@ -19,32 +19,45 @@ | |||
*/ | |||
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.exist; | |||
import static com.codeborne.selenide.Condition.hasText; | |||
import static com.codeborne.selenide.Selenide.$; | |||
import static com.codeborne.selenide.Selenide.$$; | |||
public class ProjectHistoryPage { | |||
public class ProjectActivityPage { | |||
public ProjectHistoryPage() { | |||
$("#project-history").should(exist); | |||
public ProjectActivityPage() { | |||
$("#project-activity").should(Condition.exist); | |||
} | |||
public ElementsCollection getSnapshots() { | |||
return $$("tr.snapshot"); | |||
public ElementsCollection getAnalyses() { | |||
return $$(".project-activity-analysis"); | |||
} | |||
public List<ProjectHistorySnapshotItem> getSnapshotsAsItems() { | |||
return getSnapshots() | |||
public List<ProjectAnalysisItem> getAnalysesAsItems() { | |||
return getAnalyses() | |||
.stream() | |||
.map(ProjectHistorySnapshotItem::new) | |||
.map(ProjectAnalysisItem::new) | |||
.collect(Collectors.toList()); | |||
} | |||
public void checkAlertDisplayed() { | |||
$("#info:not(.hidden)").should(exist); | |||
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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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> |
@@ -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" |
@@ -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 }) | |||
); |
@@ -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> |
@@ -98,7 +98,7 @@ export default Marionette.LayoutView.extend({ | |||
}, | |||
events: { | |||
'submit': 'onSubmit', | |||
'submit': 'handleSubmit', | |||
'keydown .js-search-input': 'onKeyDown', | |||
'keyup .js-search-input': 'onKeyUp' | |||
}, |
@@ -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} |
@@ -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) | |||
) | |||
); |
@@ -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); |
@@ -18,39 +18,36 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import { EventType } from '../propTypes'; | |||
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'; | |||
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> | |||
); | |||
}; | |||
export default class Analysis extends React.Component { | |||
props: { | |||
analysis: AnalysisType | |||
}; | |||
Event.propTypes = { | |||
event: EventType.isRequired | |||
}; | |||
render () { | |||
const { analysis } = this.props; | |||
export default Event; | |||
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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); |
@@ -319,6 +319,16 @@ | |||
box-sizing: border-box; | |||
} | |||
.overview-analysis { | |||
} | |||
.overview-analysis + .overview-analysis { | |||
margin-top: 8px; | |||
padding-top: 8px; | |||
border-top: 1px solid #e6e6e6; | |||
} | |||
/* | |||
* Other | |||
*/ |
@@ -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) | |||
); | |||
}; |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -0,0 +1,7 @@ | |||
.project-activity-event { | |||
} | |||
.project-activity-event + .project-activity-event { | |||
margin-top: 4px; | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -17,36 +17,32 @@ | |||
* 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 type { Event as EventType } from '../../../store/projectActivity/duck'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import './Event.css'; | |||
const TYPES = ['All', 'Version', 'Alert', 'Profile', 'Other']; | |||
export default class EventInner extends React.Component { | |||
props: { | |||
event: EventType | |||
}; | |||
const EventsListFilter = ({ currentFilter, onFilter }) => { | |||
const handleChange = selected => onFilter(selected.value); | |||
render () { | |||
const { event } = this.props; | |||
const options = TYPES.map(type => { | |||
return { | |||
value: type, | |||
label: translate('event.category', type) | |||
}; | |||
}); | |||
if (event.category === 'VERSION') { | |||
return ( | |||
<span className="badge project-activity-version-badge">{this.props.event.name}</span> | |||
); | |||
} | |||
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; | |||
return ( | |||
<span> | |||
<span className="note">{translate('event.category', event.category)}:</span> | |||
{' '} | |||
<strong title={event.description}>{event.name}</strong> | |||
</span> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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; | |||
} |
@@ -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}/> | |||
); |
@@ -17,40 +17,32 @@ | |||
* 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; | |||
// @flow | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import com.codeborne.selenide.SelenideElement; | |||
import org.openqa.selenium.NoSuchElementException; | |||
export default class FormattedDate extends React.Component { | |||
props: { | |||
date: string | number, | |||
format?: string, | |||
tooltipFormat?: string | |||
}; | |||
public class ProjectHistorySnapshotItem { | |||
static defaultProps = { | |||
format: 'LLL' | |||
}; | |||
private final SelenideElement elt; | |||
render () { | |||
const { date, format, tooltipFormat } = this.props; | |||
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"); | |||
} | |||
const m = moment(date); | |||
public SelenideElement getDeleteButton() { | |||
return elt.$("td:nth-child(9) input[type=\"submit\"]"); | |||
} | |||
const title = tooltipFormat ? m.format(tooltipFormat) : undefined; | |||
public void clickDelete() { | |||
getDeleteButton().click(); | |||
return ( | |||
<time dateTime={m.format()} title={title}> | |||
{m.format(format)} | |||
</time> | |||
); | |||
} | |||
} |
@@ -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" | |||
} | |||
} | |||
`; |
@@ -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" | |||
] | |||
} | |||
`; |
@@ -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 | |||
} | |||
`; |
@@ -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" | |||
} | |||
} | |||
`; |
@@ -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 | |||
} | |||
} | |||
`; |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); | |||
}); |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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] | |||
); |
@@ -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; | |||
} | |||
}; |
@@ -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] | |||
); |
@@ -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] | |||
); |
@@ -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; | |||
}; | |||
@@ -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))) | |||
); | |||
@@ -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) | |||
); |
@@ -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) | |||
); |
@@ -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; | |||
} | |||
@@ -42,20 +44,26 @@ | |||
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, |
@@ -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; |
@@ -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 |