aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-12-19 14:07:32 +0100
committerGitHub <noreply@github.com>2016-12-19 14:07:32 +0100
commit2a91ab92dac4203e4410f5b2ab0ffbd1a6efac0e (patch)
treec711f5da56f2f4ef0dd90f897112372223ece703 /server/sonar-web/src
parentafb6610a84b5f237bd036c900dd1a20ca8526311 (diff)
downloadsonarqube-2a91ab92dac4203e4410f5b2ab0ffbd1a6efac0e.tar.gz
sonarqube-2a91ab92dac4203e4410f5b2ab0ffbd1a6efac0e.zip
SONAR-7674 Add Activity Stream interface (#1459)
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/projectActivity.js99
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js28
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/SearchView.js2
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/actions.js32
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js118
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/Analysis.js (renamed from server/sonar-web/src/main/js/apps/overview/events/Event.js)59
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/EventsList.js149
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/Meta.js8
-rw-r--r--server/sonar-web/src/main/js/apps/overview/styles.css10
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/actions.js87
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ChangeIcon.js33
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/DeleteIcon.js33
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/Event.css7
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/Event.js118
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js (renamed from server/sonar-web/src/main/js/apps/overview/events/EventsListFilter.js)50
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/Events.js56
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js87
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js88
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js91
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js61
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js63
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js34
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js146
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js34
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js36
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js135
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js36
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js136
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js37
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js113
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js37
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css121
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/routes.js27
-rw-r--r--server/sonar-web/src/main/js/components/ui/FormattedDate.js48
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap93
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap55
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap75
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap71
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap21
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js90
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js80
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js100
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js90
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js49
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/analyses.js89
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js53
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/duck.js148
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/events.js79
-rw-r--r--server/sonar-web/src/main/js/store/projectActivity/paging.js34
-rw-r--r--server/sonar-web/src/main/js/store/rootActions.js2
-rw-r--r--server/sonar-web/src/main/js/store/rootReducer.js6
-rw-r--r--server/sonar-web/src/main/js/store/utils/configureStore.js4
-rw-r--r--server/sonar-web/src/main/less/components/modals.less28
-rw-r--r--server/sonar-web/src/main/less/init/forms.less18
55 files changed, 3166 insertions, 240 deletions
diff --git a/server/sonar-web/src/main/js/api/projectActivity.js b/server/sonar-web/src/main/js/api/projectActivity.js
new file mode 100644
index 00000000000..9131cb904f9
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/projectActivity.js
@@ -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 })
+);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
index c348e186615..205e203b3d5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
@@ -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>
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js b/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
index 6d30a446290..9edb27a8ff2 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
@@ -98,7 +98,7 @@ export default Marionette.LayoutView.extend({
},
events: {
- 'submit': 'onSubmit',
+ 'submit': 'handleSubmit',
'keydown .js-search-input': 'onKeyDown',
'keyup .js-search-input': 'onKeyUp'
},
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
index e92ba5d8996..2c04125c885 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.js
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -45,6 +45,7 @@ import issuesRoutes from '../../apps/issues/routes';
import metricsRoutes from '../../apps/metrics/routes';
import overviewRoutes from '../../apps/overview/routes';
import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
+import projectActivityRoutes from '../../apps/projectActivity/routes';
import projectAdminRoutes from '../../apps/project-admin/routes';
import projectsRoutes from '../../apps/projects/routes';
import projectsAdminRoutes from '../../apps/projects-admin/routes';
@@ -109,6 +110,7 @@ const startReactApp = () => {
<Route path="custom_measures">{customMeasuresRoutes}</Route>
<Route path="dashboard">{overviewRoutes}</Route>
<Route path="project">
+ <Route path="activity">{projectActivityRoutes}</Route>
<Route path="background_tasks">{backgroundTasksRoutes}</Route>
<Route path="settings">{settingsRoutes}</Route>
{projectAdminRoutes}
diff --git a/server/sonar-web/src/main/js/apps/overview/actions.js b/server/sonar-web/src/main/js/apps/overview/actions.js
new file mode 100644
index 00000000000..99136099f50
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/actions.js
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import * as api from '../../api/projectActivity';
+import { receiveProjectActivity } from '../../store/projectActivity/duck';
+import { onFail } from '../../store/rootActions';
+
+const PAGE_SIZE = 5;
+
+export const fetchRecentProjectActivity = (project: string) => (dispatch: Function) => (
+ api.getProjectActivity(project, { pageSize: PAGE_SIZE }).then(
+ ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)),
+ onFail(dispatch)
+ )
+);
diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
new file mode 100644
index 00000000000..842a9680310
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { Link } from 'react-router';
+import { connect } from 'react-redux';
+import Analysis from './Analysis';
+import { translate } from '../../../helpers/l10n';
+import { fetchRecentProjectActivity } from '../actions';
+import { getProjectActivity } from '../../../store/rootReducer';
+import { getAnalyses } from '../../../store/projectActivity/duck';
+
+type Props = {
+ analyses?: Array<*>,
+ project: string;
+ fetchRecentProjectActivity: (project: string) => Promise<*>;
+}
+
+class AnalysesList extends React.Component {
+ mounted: boolean;
+ props: Props;
+
+ state = {
+ loading: true
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ this.fetchData();
+ }
+
+ componentDidUpdate (prevProps: Props) {
+ if (prevProps.project !== this.props.project) {
+ this.fetchData();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ fetchData () {
+ this.setState({ loading: true });
+ this.props.fetchRecentProjectActivity(this.props.project).then(() => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ });
+ }
+
+ renderList (analyses) {
+ if (!analyses.length) {
+ return (
+ <p className="spacer-top note">
+ {translate('no_results')}
+ </p>
+ );
+ }
+
+ return (
+ <ul className="spacer-top">
+ {analyses.map(analysis => (
+ <Analysis key={analysis.key} analysis={analysis}/>
+ ))}
+ </ul>
+ );
+ }
+
+ render () {
+ const { analyses } = this.props;
+ const { loading } = this.state;
+
+ if (loading || !analyses) {
+ return null;
+ }
+
+ return (
+ <div className="overview-meta-card">
+ <h4 className="overview-meta-header">
+ {translate('project_activity.page')}
+ </h4>
+
+ {this.renderList(analyses)}
+
+ <div className="spacer-top small">
+ <Link to={{ pathname: '/project/activity', query: { id: this.props.project } }}>
+ {translate('show_more')}
+ </Link>
+ </div>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps: Props) => ({
+ analyses: getAnalyses(getProjectActivity(state), ownProps.project)
+});
+
+const mapDispatchToProps = { fetchRecentProjectActivity };
+
+export default connect(mapStateToProps, mapDispatchToProps)(AnalysesList);
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.js b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js
index acef6b789cb..a168b08329d 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/Event.js
+++ b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/EventsList.js b/server/sonar-web/src/main/js/apps/overview/events/EventsList.js
deleted file mode 100644
index 3b35a24d690..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/events/EventsList.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import moment from 'moment';
-import React from 'react';
-import shallowCompare from 'react-addons-shallow-compare';
-
-import Event from './Event';
-import EventsListFilter from './EventsListFilter';
-import { getEvents } from '../../../api/events';
-import { translate } from '../../../helpers/l10n';
-
-const LIMIT = 5;
-
-export default class EventsList extends React.Component {
- state = {
- events: [],
- limited: true,
- filter: 'All'
- };
-
- componentDidMount () {
- this.mounted = true;
- this.fetchEvents();
- }
-
- shouldComponentUpdate (nextProps, nextState) {
- return shallowCompare(this, nextProps, nextState);
- }
-
- componentDidUpdate (nextProps) {
- if (nextProps.component !== this.props.component) {
- this.fetchEvents();
- }
- }
-
- componentWillUnmount () {
- this.mounted = false;
- }
-
- fetchEvents () {
- getEvents(this.props.component.key).then(events => {
- if (this.mounted) {
- const nextEvents = events.map(event => {
- return {
- id: event.id,
- date: moment(event.dt).toDate(),
- type: event.c,
- name: event.n,
- text: event.ds
- };
- });
-
- this.setState({ events: nextEvents });
- }
- });
- }
-
- limitEvents (events) {
- return this.state.limited ? events.slice(0, LIMIT) : events;
- }
-
- filterEvents (events) {
- if (this.state.filter === 'All') {
- return events;
- } else {
- return events.filter(event => event.type === this.state.filter);
- }
- }
-
- handleClick (e) {
- e.preventDefault();
- this.setState({ limited: !this.state.limited });
- }
-
- handleFilter (filter) {
- this.setState({ filter });
- }
-
- renderMoreLink () {
- const text = this.state.limited ?
- translate('widget.events.show_all') :
- translate('hide');
-
- return (
- <p className="spacer-top note">
- <a onClick={this.handleClick.bind(this)} href="#">{text}</a>
- </p>
- );
- }
-
- renderList (events) {
- if (events.length) {
- return (
- <ul>
- {events.map(event => (
- <Event key={event.id} event={event}/>
- ))}
- </ul>
- );
- } else {
- return (
- <p className="spacer-top note">
- {translate('no_results')}
- </p>
- );
- }
- }
-
- render () {
- const filteredEvents = this.filterEvents(this.state.events);
- const events = this.limitEvents(filteredEvents);
-
- return (
- <div className="overview-meta-card">
- <div className="clearfix">
- <h4 className="pull-left overview-meta-header">
- {translate('widget.events.name')}
- </h4>
- <div className="pull-right">
- <EventsListFilter
- currentFilter={this.state.filter}
- onFilter={this.handleFilter.bind(this)}/>
- </div>
- </div>
-
- {this.renderList(events)}
-
- {filteredEvents.length > LIMIT && this.renderMoreLink()}
- </div>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
index c034da4452b..26be7ed8ea2 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
+++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css
index cb9c344b814..088b19ca7e2 100644
--- a/server/sonar-web/src/main/js/apps/overview/styles.css
+++ b/server/sonar-web/src/main/js/apps/overview/styles.css
@@ -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
*/
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/actions.js b/server/sonar-web/src/main/js/apps/projectActivity/actions.js
new file mode 100644
index 00000000000..83115bd1800
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/actions.js
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import * as api from '../../api/projectActivity';
+import {
+ receiveProjectActivity,
+ addEvent,
+ deleteEvent as deleteEventAction,
+ changeEvent as changeEventAction,
+ deleteAnalysis as deleteAnalysisAction,
+ getPaging
+} from '../../store/projectActivity/duck';
+import { onFail } from '../../store/rootActions';
+import { getProjectActivity } from '../../store/rootReducer';
+
+const rejectOnFail = (dispatch: Function) => (error: any) => {
+ onFail(dispatch)(error);
+ return Promise.reject();
+};
+
+export const fetchProjectActivity = (project: string, filter: ?string) => (dispatch: Function): void => {
+ api.getProjectActivity(project, { category: filter }).then(
+ ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)),
+ onFail(dispatch)
+ );
+};
+
+export const fetchMoreProjectActivity = (project: string, filter: ?string) =>
+ (dispatch: Function, getState: Function): void => {
+ const projectActivity = getProjectActivity(getState());
+ const { pageIndex } = getPaging(projectActivity, project);
+
+ api.getProjectActivity(project, { category: filter, pageIndex: pageIndex + 1 }).then(
+ ({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)),
+ onFail(dispatch)
+ );
+ };
+
+export const addCustomEvent = (analysis: string, name: string, category?: string) =>
+ (dispatch: Function): Promise<*> => {
+ return api.createEvent(analysis, name, category).then(
+ ({ analysis, ...event }) => dispatch(addEvent(analysis, event)),
+ rejectOnFail(dispatch)
+ );
+ };
+
+export const deleteEvent = (analysis: string, event: string) => (dispatch: Function): Promise<*> => {
+ return api.deleteEvent(event).then(
+ () => dispatch(deleteEventAction(analysis, event)),
+ rejectOnFail(dispatch)
+ );
+};
+
+export const addVersion = (analysis: string, version: string) => (dispatch: Function): Promise<*> => {
+ return dispatch(addCustomEvent(analysis, version, 'VERSION'));
+};
+
+export const changeEvent = (event: string, name: string) => (dispatch: Function): Promise<*> => {
+ return api.changeEvent(event, name).then(
+ () => dispatch(changeEventAction(event, { name })),
+ rejectOnFail(dispatch)
+ );
+};
+
+export const deleteAnalysis = (project: string, analysis: string) => (dispatch: Function): Promise<*> => {
+ return api.deleteAnalysis(analysis).then(
+ () => dispatch(deleteAnalysisAction(project, analysis)),
+ rejectOnFail(dispatch)
+ );
+};
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ChangeIcon.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ChangeIcon.js
new file mode 100644
index 00000000000..c9a0862e094
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ChangeIcon.js
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+
+export default class ChangeIcon extends React.Component {
+ render () {
+ /* eslint-disable max-len */
+ return (
+ <svg width="12" height="12" viewBox="0 0 14 14">
+ <path fill="#236a97"
+ d="M3.35 12.82l.85-.84L2.02 9.8l-.84.85v.98h1.2v1.2h.97zM8.2 4.24c0-.13-.08-.2-.22-.2-.06 0-.1.02-.15.06l-5 5c-.05.05-.08.1-.08.17 0 .13.07.2.2.2.07 0 .12-.02.16-.06l5.02-5c.05-.04.07-.1.07-.16zm-.5-1.77l3.83 3.84-7.7 7.7H0v-3.84l7.7-7.7zm6.3.88c0 .33-.1.6-.34.84L12.12 5.7 8.28 1.88 9.8.35c.24-.23.5-.35.85-.35.32 0 .6.12.84.35l2.16 2.16c.23.25.34.53.34.85z"/>
+ </svg>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/DeleteIcon.js b/server/sonar-web/src/main/js/apps/projectActivity/components/DeleteIcon.js
new file mode 100644
index 00000000000..fc0ea426cd8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/DeleteIcon.js
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+
+export default class DeleteIcon extends React.Component {
+ render () {
+ /* eslint-disable max-len */
+ return (
+ <svg width="12" height="12" viewBox="0 0 14 14">
+ <path fill="#d4333f"
+ d="M14 11.27c0 .3-.1.58-.33.8l-1.6 1.6c-.22.22-.5.33-.8.33-.32 0-.6-.1-.8-.33L7 10.2l-3.46 3.47c-.22.22-.5.33-.8.33-.32 0-.6-.1-.8-.33l-1.6-1.6c-.23-.22-.34-.5-.34-.8 0-.32.1-.6.33-.8L3.8 7 .32 3.54C.1 3.32 0 3.04 0 2.74c0-.32.1-.6.33-.8l1.6-1.6c.22-.23.5-.34.8-.34.32 0 .6.1.8.33L7 3.8 10.46.32c.22-.22.5-.33.8-.33.32 0 .6.1.8.33l1.6 1.6c.23.22.34.5.34.8 0 .32-.1.6-.33.8L10.2 7l3.47 3.46c.22.22.33.5.33.8z"/>
+ </svg>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.css b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.css
new file mode 100644
index 00000000000..0a28479b429
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.css
@@ -0,0 +1,7 @@
+.project-activity-event {
+
+}
+
+.project-activity-event + .project-activity-event {
+ margin-top: 4px;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js
new file mode 100644
index 00000000000..cb0f23613a6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import EventInner from './EventInner';
+import ChangeCustomEventForm from './forms/ChangeCustomEventForm';
+import RemoveCustomEventForm from './forms/RemoveCustomEventForm';
+import DeleteIcon from './DeleteIcon';
+import ChangeIcon from './ChangeIcon';
+import type { Event as EventType } from '../../../store/projectActivity/duck';
+
+type Props = {
+ analysis: string,
+ event: EventType,
+ isFirst: boolean,
+ canAdmin: boolean
+};
+
+type State = {
+ changing: boolean,
+ deleting: boolean
+};
+
+export default class Event extends React.Component {
+ mounted: boolean;
+ props: Props;
+
+ state: State = {
+ changing: false,
+ deleting: false
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ startChanging = () => {
+ this.setState({ changing: true });
+ };
+
+ stopChanging = () => {
+ if (this.mounted) {
+ this.setState({ changing: false });
+ }
+ };
+
+ startDeleting = () => {
+ this.setState({ deleting: true });
+ };
+
+ stopDeleting = () => {
+ if (this.mounted) {
+ this.setState({ deleting: false });
+ }
+ };
+
+ render () {
+ const { event, canAdmin } = this.props;
+ const canChange = ['OTHER', 'VERSION'].includes(event.category);
+ const canDelete = event.category === 'OTHER' || (event.category === 'VERSION' && !this.props.isFirst);
+ const showActions = canAdmin && (canChange || canDelete);
+
+ return (
+ <div className="project-activity-event">
+ <EventInner event={this.props.event}/>
+
+ {showActions && (
+ <div className="project-activity-event-actions">
+ {canChange && (
+ <button className="js-change-event button-clean" onClick={this.startChanging}>
+ <ChangeIcon/>
+ </button>
+ )}
+ {canDelete && (
+ <button className="js-delete-event button-clean" onClick={this.startDeleting}>
+ <DeleteIcon/>
+ </button>
+ )}
+ </div>
+ )}
+
+ {this.state.changing && (
+ <ChangeCustomEventForm
+ event={this.props.event}
+ onClose={this.stopChanging}/>
+ )}
+
+ {this.state.deleting && (
+ <RemoveCustomEventForm
+ analysis={this.props.analysis}
+ event={this.props.event}
+ onClose={this.stopDeleting}/>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/EventsListFilter.js b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js
index cfd142fd3e8..61dbfa32124 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/EventsListFilter.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js
new file mode 100644
index 00000000000..7452f2629e6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import sortBy from 'lodash/sortBy';
+import Event from './Event';
+import type { Event as EventType } from '../../../store/projectActivity/duck';
+
+export default class Events extends React.Component {
+ props: {
+ analysis: string,
+ events: Array<EventType>,
+ isFirst: boolean,
+ canAdmin: boolean
+ };
+
+ render () {
+ const sortedEvents: Array<EventType> = sortBy(
+ this.props.events,
+ // versions first
+ (event: EventType) => event.category === 'VERSION' ? 0 : 1,
+ // then the rest sorted by category
+ 'category'
+ );
+
+ return (
+ <div className="project-activity-events">
+ {sortedEvents.map(event => (
+ <Event
+ key={event.key}
+ analysis={this.props.analysis}
+ event={event}
+ isFirst={this.props.isFirst}
+ canAdmin={this.props.canAdmin}/>
+ ))}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
new file mode 100644
index 00000000000..91b827d877b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import groupBy from 'lodash/groupBy';
+import moment from 'moment';
+import ProjectActivityAnalysis from './ProjectActivityAnalysis';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import { getProjectActivity } from '../../../store/rootReducer';
+import { getAnalyses } from '../../../store/projectActivity/duck';
+import { translate } from '../../../helpers/l10n';
+
+class ProjectActivityAnalysesList extends React.Component {
+ props: {
+ project: string,
+ analyses?: Array<{
+ key: string,
+ date: string
+ }>,
+ canAdmin: boolean
+ };
+
+ render () {
+ if (!this.props.analyses) {
+ return null;
+ }
+
+ if (this.props.analyses.length === 0) {
+ return (
+ <div className="note">{translate('no_results')}</div>
+ );
+ }
+
+ const firstAnalysis = this.props.analyses[0];
+
+ const byDay = groupBy(this.props.analyses, analysis => moment(analysis.date).startOf('day').valueOf());
+
+ return (
+ <div className="boxed-group boxed-group-inner">
+ <ul className="project-activity-days-list">
+ {Object.keys(byDay).map(day => (
+ <li key={day} className="project-activity-day" data-day={moment(Number(day)).format('YYYY-MM-DD')}>
+ <div className="project-activity-date">
+ <FormattedDate date={Number(day)} format="LL"/>
+ </div>
+
+ <ul className="project-activity-analyses-list">
+ {byDay[day].map(analysis => (
+ <ProjectActivityAnalysis
+ key={analysis.key}
+ analysis={analysis}
+ isFirst={analysis === firstAnalysis}
+ project={this.props.project}
+ canAdmin={this.props.canAdmin}/>
+ ))}
+ </ul>
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+ analyses: getAnalyses(getProjectActivity(state), ownProps.project)
+});
+
+export default connect(mapStateToProps)(ProjectActivityAnalysesList);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
new file mode 100644
index 00000000000..23e6915a8ad
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Events from './Events';
+import AddVersionForm from './forms/AddVersionForm';
+import AddCustomEventForm from './forms/AddCustomEventForm';
+import RemoveAnalysisForm from './forms/RemoveAnalysisForm';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import type { Analysis } from '../../../store/projectActivity/duck';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProjectActivityAnalysis extends React.Component {
+ props: {
+ analysis: Analysis,
+ isFirst: boolean,
+ project: string,
+ canAdmin: boolean
+ };
+
+ render () {
+ const { date, events } = this.props.analysis;
+ const { isFirst, canAdmin } = this.props;
+
+ const version = events.find(event => event.category === 'VERSION');
+
+ return (
+ <li className="project-activity-analysis clearfix">
+ {canAdmin && (
+ <div className="project-activity-analysis-actions">
+ <div className="dropdown display-inline-block">
+ <button className="js-create button-small" data-toggle="dropdown">
+ {translate('create')} <i className="icon-dropdown"/>
+ </button>
+ <ul className="dropdown-menu dropdown-menu-right">
+ {version == null && (
+ <li>
+ <AddVersionForm analysis={this.props.analysis}/>
+ </li>
+ )}
+ <li>
+ <AddCustomEventForm analysis={this.props.analysis}/>
+ </li>
+ </ul>
+ </div>
+
+ {!isFirst && (
+ <div className="display-inline-block little-spacer-left">
+ <RemoveAnalysisForm
+ analysis={this.props.analysis}
+ project={this.props.project}/>
+ </div>
+ )}
+ </div>
+ )}
+
+ <div className="project-activity-time">
+ <FormattedDate date={date} format="LT" tooltipFormat="LTS"/>
+ </div>
+
+ {events.length > 0 && (
+ <Events
+ analysis={this.props.analysis.key}
+ events={events}
+ isFirst={this.props.isFirst}
+ canAdmin={canAdmin}/>
+ )}
+ </li>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
new file mode 100644
index 00000000000..6dc24183a22
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import ProjectActivityPageHeader from './ProjectActivityPageHeader';
+import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
+import ProjectActivityPageFooter from './ProjectActivityPageFooter';
+import { fetchProjectActivity } from '../actions';
+import { getComponent } from '../../../store/rootReducer';
+import './projectActivity.css';
+
+type Props = {
+ location: { query: { id: string } },
+ fetchProjectActivity: (project: string) => void,
+ filter: ?string,
+ project: { configuration?: { showHistory: boolean } }
+};
+
+type State = {
+ filter: ?string
+};
+
+class ProjectActivityApp extends React.Component {
+ props: Props;
+
+ state: State = {
+ filter: null
+ };
+
+ componentDidMount () {
+ document.querySelector('html').classList.add('dashboard-page');
+ this.props.fetchProjectActivity(this.props.location.query.id);
+ }
+
+ componentWillUnmount () {
+ document.querySelector('html').classList.remove('dashboard-page');
+ }
+
+ handleFilter = (filter: ?string) => {
+ this.setState({ filter });
+ this.props.fetchProjectActivity(this.props.location.query.id, filter);
+ };
+
+ render () {
+ const project = this.props.location.query.id;
+ const { configuration } = this.props.project;
+ const canAdmin = configuration ? configuration.showHistory : false;
+
+ return (
+ <div id="project-activity" className="page page-limited">
+ <ProjectActivityPageHeader
+ project={project}
+ filter={this.state.filter}
+ changeFilter={this.handleFilter}/>
+
+ <ProjectActivityAnalysesList
+ project={project}
+ canAdmin={canAdmin}/>
+
+ <ProjectActivityPageFooter
+ project={project}/>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps: Props) => ({
+ project: getComponent(state, ownProps.location.query.id)
+});
+
+const mapDispatchToProps = { fetchProjectActivity };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ProjectActivityApp);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js
new file mode 100644
index 00000000000..a487c995faa
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import ListFooter from '../../../components/controls/ListFooter';
+import { getProjectActivity } from '../../../store/rootReducer';
+import { getAnalyses, getPaging } from '../../../store/projectActivity/duck';
+import { fetchMoreProjectActivity } from '../actions';
+import type { Paging } from '../../../store/projectActivity/duck';
+
+class ProjectActivityPageFooter extends React.Component {
+ props: {
+ analyses: Array<*>,
+ paging: ?Paging,
+ project: string,
+ fetchMoreProjectActivity: (project: string) => void
+ };
+
+ handleLoadMore = () => {
+ this.props.fetchMoreProjectActivity(this.props.project);
+ };
+
+ render () {
+ const { analyses, paging } = this.props;
+
+ if (!paging || analyses.length === 0) {
+ return null;
+ }
+
+ return (
+ <ListFooter count={analyses.length} total={paging.total} loadMore={this.handleLoadMore}/>
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+ analyses: getAnalyses(getProjectActivity(state), ownProps.project),
+ paging: getPaging(getProjectActivity(state), ownProps.project)
+});
+
+const mapDispatchToProps = { fetchMoreProjectActivity };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ProjectActivityPageFooter);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
new file mode 100644
index 00000000000..091a02950f9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Select from 'react-select';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+ changeFilter: (filter: ?string) => void,
+ filter: ?string,
+ project: string
+};
+
+export default class ProjectActivityPageHeader extends React.Component {
+ props: Props;
+
+ handleChange = (option: null | { value: string }) => {
+ this.props.changeFilter(option && option.value);
+ }
+
+ render () {
+ const selectOptions = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'].map(category => ({
+ label: translate('event.category', category),
+ value: category
+ }));
+
+ return (
+ <header className="page-header">
+ <div className="page-actions">
+ <Select
+ className="input-medium"
+ placeholder={translate('filter_verb') + '...'}
+ clearable={true}
+ searchable={false}
+ value={this.props.filter}
+ options={selectOptions}
+ onChange={this.handleChange}/>
+ </div>
+
+ <div className="page-description">
+ {translate('project_activity.page.description')}
+ </div>
+ </header>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js
new file mode 100644
index 00000000000..4d4f287e6f2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddCustomEventForm.js
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import { addCustomEvent } from '../../actions';
+import AddEventForm from './AddEventForm';
+
+const AddCustomEventForm = props => (
+ <AddEventForm {...props} addEventButtonText="project_activity.add_custom_event"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { addEvent: addCustomEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddCustomEventForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
new file mode 100644
index 00000000000..da895b642b4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import type { Analysis } from '../../../../store/projectActivity/duck';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {
+ addEvent: () => Promise<*>,
+ analysis: Analysis,
+ addEventButtonText: string
+};
+
+type State = {
+ open: boolean,
+ processing: boolean;
+ name: string;
+}
+
+export default class AddEventForm extends React.Component {
+ mounted: boolean;
+ props: Props;
+
+ state: State = {
+ open: false,
+ processing: false,
+ name: ''
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ openForm = (e: Object) => {
+ e.preventDefault();
+ if (this.mounted) {
+ this.setState({ open: true });
+ }
+ };
+
+ closeForm = () => {
+ if (this.mounted) {
+ this.setState({ open: false, name: '' });
+ }
+ };
+
+ changeInput = (e: Object) => {
+ if (this.mounted) {
+ this.setState({ name: e.target.value });
+ }
+ };
+
+ stopProcessing = () => {
+ if (this.mounted) {
+ this.setState({ processing: false });
+ }
+ };
+
+ stopProcessingAndClose = () => {
+ if (this.mounted) {
+ this.setState({ open: false, processing: false, name: '' });
+ }
+ };
+
+ handleSubmit = (e: Object) => {
+ e.preventDefault();
+ this.setState({ processing: true });
+ this.props.addEvent(this.props.analysis.key, this.state.name)
+ .then(this.stopProcessingAndClose, this.stopProcessing);
+ };
+
+ renderModal () {
+ return (
+ <Modal isOpen={true}
+ contentLabel="modal form"
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.closeForm}>
+
+ <header className="modal-head">
+ <h2>{translate(this.props.addEventButtonText)}</h2>
+ </header>
+
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ <div className="modal-field">
+ <label>{translate('name')}</label>
+ <input
+ value={this.state.name}
+ autoFocus={true}
+ disabled={this.state.processing}
+ className="input-medium"
+ type="text"
+ onChange={this.changeInput}/>
+ </div>
+ </div>
+
+ <footer className="modal-foot">
+ {this.state.processing ? (
+ <i className="spinner"/>
+ ) : (
+ <div>
+ <button type="submit">{translate('save')}</button>
+ <button type="reset" className="button-link" onClick={this.closeForm}>
+ {translate('cancel')}
+ </button>
+ </div>
+ )}
+ </footer>
+ </form>
+
+ </Modal>
+ );
+ }
+
+ render () {
+ return (
+ <a className="js-add-event button-small" href="#" onClick={this.openForm}>
+ {translate(this.props.addEventButtonText)}
+ {this.state.open && this.renderModal()}
+ </a>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js
new file mode 100644
index 00000000000..8443d0b45b0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddVersionForm.js
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import { addVersion } from '../../actions';
+import AddEventForm from './AddEventForm';
+
+const AddVersionForm = props => (
+ <AddEventForm {...props} addEventButtonText="project_activity.add_version"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { addEvent: addVersion };
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddVersionForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js
new file mode 100644
index 00000000000..df4a93ebee8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeCustomEventForm.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import ChangeEventForm from './ChangeEventForm';
+import { changeEvent } from '../../actions';
+
+const ChangeCustomEventForm = props => (
+ <ChangeEventForm
+ {...props}
+ changeEventButtonText="project_activity.change_custom_event"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { changeEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ChangeCustomEventForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js
new file mode 100644
index 00000000000..8ae9e6b1cb4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.js
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import type { Event } from '../../../../store/projectActivity/duck';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {
+ changeEvent: () => Promise<*>,
+ changeEventButtonText: string,
+ event: Event,
+ onClose: () => void
+};
+
+type State = {
+ processing: boolean,
+ name: string
+}
+
+export default class ChangeEventForm extends React.Component {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ constructor (props: Props) {
+ super(props);
+ this.state = {
+ processing: false,
+ name: props.event.name
+ };
+ }
+
+ componentDidMount () {
+ this.mounted = true;
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ closeForm = () => {
+ if (this.mounted) {
+ this.setState({ name: this.props.event.name });
+ }
+ this.props.onClose();
+ };
+
+ changeInput = (e: Object) => {
+ if (this.mounted) {
+ this.setState({ name: e.target.value });
+ }
+ };
+
+ stopProcessing = () => {
+ if (this.mounted) {
+ this.setState({ processing: false });
+ }
+ };
+
+ stopProcessingAndClose = () => {
+ if (this.mounted) {
+ this.setState({ processing: false });
+ }
+ this.props.onClose();
+ };
+
+ handleSubmit = (e: Object) => {
+ e.preventDefault();
+ this.setState({ processing: true });
+ this.props.changeEvent(this.props.event.key, this.state.name)
+ .then(this.stopProcessingAndClose, this.stopProcessing);
+ };
+
+ render () {
+ return (
+ <Modal isOpen={true}
+ contentLabel="modal form"
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.closeForm}>
+
+ <header className="modal-head">
+ <h2>{translate(this.props.changeEventButtonText)}</h2>
+ </header>
+
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ <div className="modal-field">
+ <label>{translate('name')}</label>
+ <input
+ value={this.state.name}
+ autoFocus={true}
+ disabled={this.state.processing}
+ className="input-medium"
+ type="text"
+ onChange={this.changeInput}/>
+ </div>
+ </div>
+
+ <footer className="modal-foot">
+ {this.state.processing ? (
+ <i className="spinner"/>
+ ) : (
+ <div>
+ <button type="submit">{translate('change_verb')}</button>
+ <button type="reset" className="button-link" onClick={this.closeForm}>
+ {translate('cancel')}
+ </button>
+ </div>
+ )}
+ </footer>
+ </form>
+
+ </Modal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js
new file mode 100644
index 00000000000..e4a088dbc68
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeVersionForm.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import ChangeEventForm from './ChangeEventForm';
+import { changeEvent } from '../../actions';
+
+const ChangeVersionForm = props => (
+ <ChangeEventForm
+ {...props}
+ changeEventButtonText="project_activity.change_version"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { changeEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ChangeVersionForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
new file mode 100644
index 00000000000..482362772ff
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import Modal from 'react-modal';
+import type { Analysis } from '../../../../store/projectActivity/duck';
+import { translate } from '../../../../helpers/l10n';
+import { deleteAnalysis } from '../../actions';
+
+type Props = {
+ analysis: Analysis,
+ deleteAnalysis: () => Promise<*>,
+ project: string
+};
+
+type State = {
+ open: boolean,
+ processing: boolean
+}
+
+class RemoveAnalysisForm extends React.Component {
+ mounted: boolean;
+ props: Props;
+
+ state: State = {
+ open: false,
+ processing: false
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ openForm = () => {
+ if (this.mounted) {
+ this.setState({ open: true });
+ }
+ };
+
+ closeForm = () => {
+ if (this.mounted) {
+ this.setState({ open: false });
+ }
+ };
+
+ stopProcessing = () => {
+ if (this.mounted) {
+ this.setState({ processing: false });
+ }
+ };
+
+ stopProcessingAndClose = () => {
+ if (this.mounted) {
+ this.setState({ open: false, processing: false });
+ }
+ };
+
+ handleSubmit = (e: Object) => {
+ e.preventDefault();
+ this.setState({ processing: true });
+ this.props.deleteAnalysis(this.props.project, this.props.analysis.key)
+ .then(this.stopProcessingAndClose, this.stopProcessing);
+ };
+
+ renderModal () {
+ return (
+ <Modal isOpen={true}
+ contentLabel="modal form"
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.closeForm}>
+
+ <header className="modal-head">
+ <h2>{translate('project_activity.delete_analysis')}</h2>
+ </header>
+
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ {translate('project_activity.delete_analysis.question')}
+ </div>
+
+ <footer className="modal-foot">
+ {this.state.processing ? (
+ <i className="spinner"/>
+ ) : (
+ <div>
+ <button type="submit" className="button-red">{translate('delete')}</button>
+ <button type="reset" className="button-link" onClick={this.closeForm}>
+ {translate('cancel')}
+ </button>
+ </div>
+ )}
+ </footer>
+ </form>
+
+ </Modal>
+ );
+ }
+
+ render () {
+ return (
+ <button className="js-delete-analysis button-small button-red" onClick={this.openForm}>
+ {translate('delete')}
+ {this.state.open && this.renderModal()}
+ </button>
+ );
+ }
+}
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { deleteAnalysis };
+
+export default connect(mapStateToProps, mapDispatchToProps)(RemoveAnalysisForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js
new file mode 100644
index 00000000000..e9679b08207
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveCustomEventForm.js
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import RemoveEventForm from './RemoveEventForm';
+import { deleteEvent } from '../../actions';
+
+const RemoveCustomEventForm = props => (
+ <RemoveEventForm
+ {...props}
+ removeEventButtonText="project_activity.remove_custom_event"
+ removeEventQuestion="project_activity.remove_custom_event.question"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { deleteEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(RemoveCustomEventForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js
new file mode 100644
index 00000000000..893d164b7af
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.js
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import type { Analysis, Event } from '../../../../store/projectActivity/duck';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {
+ analysis: Analysis,
+ deleteEvent: () => Promise<*>,
+ event: Event,
+ removeEventButtonText: string,
+ removeEventQuestion: string,
+ onClose: () => void
+};
+
+type State = {
+ processing: boolean
+}
+
+export default class RemoveVersionForm extends React.Component {
+ mounted: boolean;
+ props: Props;
+
+ state: State = {
+ processing: false
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ closeForm = () => {
+ this.props.onClose();
+ };
+
+ stopProcessing = () => {
+ if (this.mounted) {
+ this.setState({ processing: false });
+ }
+ };
+
+ stopProcessingAndClose = () => {
+ if (this.mounted) {
+ this.setState({ processing: false });
+ }
+ this.props.onClose();
+ };
+
+ handleSubmit = (e: Object) => {
+ e.preventDefault();
+ this.setState({ processing: true });
+ this.props.deleteEvent(this.props.analysis, this.props.event.key)
+ .then(this.stopProcessingAndClose, this.stopProcessing);
+ };
+
+ render () {
+ return (
+ <Modal isOpen={true}
+ contentLabel="modal form"
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.closeForm}>
+
+ <header className="modal-head">
+ <h2>{translate(this.props.removeEventButtonText)}</h2>
+ </header>
+
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ {translate(this.props.removeEventQuestion)}
+ </div>
+
+ <footer className="modal-foot">
+ {this.state.processing ? (
+ <i className="spinner"/>
+ ) : (
+ <div>
+ <button type="submit" className="button-red" autoFocus={true}>{translate('delete')}</button>
+ <button type="reset" className="button-link" onClick={this.closeForm}>
+ {translate('cancel')}
+ </button>
+ </div>
+ )}
+ </footer>
+ </form>
+
+ </Modal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js
new file mode 100644
index 00000000000..8d1d6bc3e50
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveVersionForm.js
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { connect } from 'react-redux';
+import RemoveEventForm from './RemoveEventForm';
+import { deleteEvent } from '../../actions';
+
+const RemoveVersionForm = props => (
+ <RemoveEventForm
+ {...props}
+ removeEventButtonText="project_activity.remove_version"
+ removeEventQuestion="project_activity.remove_version.question"/>
+);
+
+const mapStateToProps = null;
+
+const mapDispatchToProps = { deleteEvent };
+
+export default connect(mapStateToProps, mapDispatchToProps)(RemoveVersionForm);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
new file mode 100644
index 00000000000..79e426e0172
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
@@ -0,0 +1,121 @@
+.project-activity-days-list {
+
+}
+
+.project-activity-day {
+ margin-bottom: 40px;
+}
+
+.project-activity-date {
+ margin-bottom: 16px;
+ font-size: 15px;
+ font-weight: bold;
+}
+
+.project-activity-analyses-list {
+
+}
+
+.project-activity-analysis {
+ position: relative;
+ min-height: 20px;
+ padding-top: 6px;
+ padding-bottom: 6px;
+ border-top: 1px solid #e6e6e6;
+ border-bottom: 1px solid #e6e6e6;
+}
+
+.project-activity-analysis:hover {
+ background-color: #ecf6fe;
+}
+
+.project-activity-analysis + .project-activity-analysis {
+ border-top: none;
+}
+
+.project-activity-analysis-actions {
+ float: right;
+ padding-right: 10px;
+}
+
+.project-activity-analysis-actions:first-child,
+.project-activity-analysis-actions:empty {
+ margin-top: 0;
+}
+
+.project-activity-analysis-actions > button + button,
+.project-activity-analysis-actions > button + form,
+.project-activity-analysis-actions > form + button,
+.project-activity-analysis-actions > form + form {
+ margin-left: 8px;
+}
+
+.project-activity-analysis-form {
+ display: inline-block;
+ vertical-align: top;
+ line-height: 20px;
+ margin-bottom: 10px;
+ padding: 9px;
+ border: 1px solid #faebcc;
+ border-radius: 2px;
+ background-color: #fcf8e3;
+}
+
+.project-activity-analysis-form + .project-activity-analysis-form {
+ margin-left: 8px;
+}
+
+.project-activity-time {
+ float: left;
+ width: 130px;
+ line-height: 20px;
+ padding-right: 50px;
+ box-sizing: border-box;
+ font-size: 12px;
+ font-weight: bold;
+ text-align: right;
+}
+
+.project-activity-time::after {
+ position: absolute;
+ z-index: 21;
+ top: 11px;
+ left: 100px;
+ display: block;
+ width: 10px;
+ height: 10px;
+ border: 2px solid #4b9fd5;
+ border-radius: 10px;
+ box-sizing: border-box;
+ content: "";
+}
+
+.project-activity-events {
+ overflow: hidden;
+}
+
+.project-activity-event {
+ line-height: 20px;
+}
+
+.project-activity-event-actions {
+ display: inline-block;
+ margin-left: 8px;
+}
+
+.project-activity-event-actions button {
+ height: 20px;
+}
+
+.project-activity-event-actions button + button {
+ margin-left: 4px;
+}
+
+.project-activity-version-badge {
+ vertical-align: middle;
+ padding: 4px 8px;
+ border-radius: 2px;
+ font-weight: bold;
+ font-size: 12px;
+ letter-spacing: 0;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/routes.js b/server/sonar-web/src/main/js/apps/projectActivity/routes.js
new file mode 100644
index 00000000000..a6bb1db8211
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/routes.js
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { IndexRoute } from 'react-router';
+import ProjectActivityApp from './components/ProjectActivityApp';
+
+export default (
+ <IndexRoute component={ProjectActivityApp}/>
+);
diff --git a/server/sonar-web/src/main/js/components/ui/FormattedDate.js b/server/sonar-web/src/main/js/components/ui/FormattedDate.js
new file mode 100644
index 00000000000..77949764c73
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/FormattedDate.js
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import moment from 'moment';
+
+export default class FormattedDate extends React.Component {
+ props: {
+ date: string | number,
+ format?: string,
+ tooltipFormat?: string
+ };
+
+ static defaultProps = {
+ format: 'LLL'
+ };
+
+ render () {
+ const { date, format, tooltipFormat } = this.props;
+
+ const m = moment(date);
+
+ const title = tooltipFormat ? m.format(tooltipFormat) : undefined;
+
+ return (
+ <time dateTime={m.format()} title={title}>
+ {m.format(format)}
+ </time>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap
new file mode 100644
index 00000000000..2f807df6231
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analyses-test.js.snap
@@ -0,0 +1,93 @@
+exports[`test reducer 1`] = `Object {}`;
+
+exports[`test reducer 2`] = `
+Object {
+ "AVgAgC1Vdo07z3PUnnkt": Object {
+ "date": "2016-10-26T12:17:29+0200",
+ "events": Array [
+ "AVkWNYNYr4pSN7TrXcjY"
+ ],
+ "key": "AVgAgC1Vdo07z3PUnnkt"
+ },
+ "AVgFqeOSKpGuA48ADATE": Object {
+ "date": "2016-10-27T12:21:15+0200",
+ "events": Array [],
+ "key": "AVgFqeOSKpGuA48ADATE"
+ },
+ "AVgGkRvCrrTJiPpCD-rG": Object {
+ "date": "2016-10-27T16:33:50+0200",
+ "events": Array [
+ "AVjUDBiSiXOcXjpycvde"
+ ],
+ "key": "AVgGkRvCrrTJiPpCD-rG"
+ }
+}
+`;
+
+exports[`test reducer 3`] = `
+Object {
+ "AVgAgC1Vdo07z3PUnnkt": Object {
+ "date": "2016-10-26T12:17:29+0200",
+ "events": Array [
+ "AVkWNYNYr4pSN7TrXcjY"
+ ],
+ "key": "AVgAgC1Vdo07z3PUnnkt"
+ },
+ "AVgFqeOSKpGuA48ADATE": Object {
+ "date": "2016-10-27T12:21:15+0200",
+ "events": Array [],
+ "key": "AVgFqeOSKpGuA48ADATE"
+ },
+ "AVgGkRvCrrTJiPpCD-rG": Object {
+ "date": "2016-10-27T16:33:50+0200",
+ "events": Array [
+ "AVjUDBiSiXOcXjpycvde",
+ "AVkWcQ8Hr4pSN7TrXcjZ"
+ ],
+ "key": "AVgGkRvCrrTJiPpCD-rG"
+ }
+}
+`;
+
+exports[`test reducer 4`] = `
+Object {
+ "AVgAgC1Vdo07z3PUnnkt": Object {
+ "date": "2016-10-26T12:17:29+0200",
+ "events": Array [
+ "AVkWNYNYr4pSN7TrXcjY"
+ ],
+ "key": "AVgAgC1Vdo07z3PUnnkt"
+ },
+ "AVgFqeOSKpGuA48ADATE": Object {
+ "date": "2016-10-27T12:21:15+0200",
+ "events": Array [],
+ "key": "AVgFqeOSKpGuA48ADATE"
+ },
+ "AVgGkRvCrrTJiPpCD-rG": Object {
+ "date": "2016-10-27T16:33:50+0200",
+ "events": Array [
+ "AVjUDBiSiXOcXjpycvde"
+ ],
+ "key": "AVgGkRvCrrTJiPpCD-rG"
+ }
+}
+`;
+
+exports[`test reducer 5`] = `
+Object {
+ "AVgAgC1Vdo07z3PUnnkt": Object {
+ "date": "2016-10-26T12:17:29+0200",
+ "events": Array [
+ "AVkWNYNYr4pSN7TrXcjY"
+ ],
+ "key": "AVgAgC1Vdo07z3PUnnkt"
+ },
+ "AVgGkRvCrrTJiPpCD-rG": Object {
+ "date": "2016-10-27T16:33:50+0200",
+ "events": Array [
+ "AVjUDBiSiXOcXjpycvde"
+ ],
+ "key": "AVgGkRvCrrTJiPpCD-rG"
+ }
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap
new file mode 100644
index 00000000000..f4fafe25e16
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/analysesByProject-test.js.snap
@@ -0,0 +1,55 @@
+exports[`test reducer 1`] = `Object {}`;
+
+exports[`test reducer 2`] = `
+Object {
+ "project-foo": Array [
+ "AVgFqeOSKpGuA48ADATE",
+ "AVgAgC1Vdo07z3PUnnkt"
+ ]
+}
+`;
+
+exports[`test reducer 3`] = `
+Object {
+ "project-foo": Array [
+ "AVgFqeOSKpGuA48ADATE",
+ "AVgAgC1Vdo07z3PUnnkt",
+ "AVgFqeOSKpGuA48ADATX"
+ ]
+}
+`;
+
+exports[`test reducer 4`] = `
+Object {
+ "project-bar": Array [
+ "AVgGkRvCrrTJiPpCD-rG"
+ ],
+ "project-foo": Array [
+ "AVgFqeOSKpGuA48ADATE",
+ "AVgAgC1Vdo07z3PUnnkt",
+ "AVgFqeOSKpGuA48ADATX"
+ ]
+}
+`;
+
+exports[`test reducer 5`] = `
+Object {
+ "project-bar": Array [
+ "AVgGkRvCrrTJiPpCD-rG"
+ ],
+ "project-foo": Array [
+ "AVgAgC1Vdo07z3PUnnkt",
+ "AVgFqeOSKpGuA48ADATX"
+ ]
+}
+`;
+
+exports[`test reducer 6`] = `
+Object {
+ "project-bar": Array [],
+ "project-foo": Array [
+ "AVgAgC1Vdo07z3PUnnkt",
+ "AVgFqeOSKpGuA48ADATX"
+ ]
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap
new file mode 100644
index 00000000000..c547f468588
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/duck-test.js.snap
@@ -0,0 +1,75 @@
+exports[`actions addEvent 1`] = `
+Object {
+ "analysis": "foo",
+ "event": Object {
+ "key": "bar"
+ },
+ "type": "ADD_PROJECT_ACTIVITY_EVENT"
+}
+`;
+
+exports[`actions changeEvent 1`] = `
+Object {
+ "changes": Object {
+ "name": "bar"
+ },
+ "event": "foo",
+ "type": "CHANGE_PROJECT_ACTIVITY_EVENT"
+}
+`;
+
+exports[`actions deleteAnalysis 1`] = `
+Object {
+ "analysis": "bar",
+ "project": "foo",
+ "type": "DELETE_PROJECT_ACTIVITY_ANALYSIS"
+}
+`;
+
+exports[`actions deleteEvent 1`] = `
+Object {
+ "analysis": "foo",
+ "event": "bar",
+ "type": "DELETE_PROJECT_ACTIVITY_EVENT"
+}
+`;
+
+exports[`selectors getAnalyses 1`] = `
+Array [
+ Object {
+ "date": "2016-10-27T16:33:50+0200",
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "AVjUDBiSiXOcXjpycvde",
+ "name": "2.18-SNAPSHOT"
+ }
+ ],
+ "key": "AVgGkRvCrrTJiPpCD-rG"
+ },
+ Object {
+ "date": "2016-10-27T12:21:15+0200",
+ "events": Array [],
+ "key": "AVgFqeOSKpGuA48ADATE"
+ },
+ Object {
+ "date": "2016-10-26T12:17:29+0200",
+ "events": Array [
+ Object {
+ "category": "OTHER",
+ "key": "AVkWNYNYr4pSN7TrXcjY",
+ "name": "foo"
+ }
+ ],
+ "key": "AVgAgC1Vdo07z3PUnnkt"
+ }
+]
+`;
+
+exports[`selectors getPaging 1`] = `
+Object {
+ "pageIndex": 1,
+ "pageSize": 100,
+ "total": 3
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap
new file mode 100644
index 00000000000..78327b2ddc4
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/events-test.js.snap
@@ -0,0 +1,71 @@
+exports[`test reducer 1`] = `Object {}`;
+
+exports[`test reducer 2`] = `
+Object {
+ "AVjUDBiSiXOcXjpycvde": Object {
+ "category": "VERSION",
+ "key": "AVjUDBiSiXOcXjpycvde",
+ "name": "2.18-SNAPSHOT"
+ },
+ "AVkWNYNYr4pSN7TrXcjY": Object {
+ "category": "OTHER",
+ "key": "AVkWNYNYr4pSN7TrXcjY",
+ "name": "foo"
+ }
+}
+`;
+
+exports[`test reducer 3`] = `
+Object {
+ "AVjUDBiSiXOcXjpycvde": Object {
+ "category": "VERSION",
+ "key": "AVjUDBiSiXOcXjpycvde",
+ "name": "2.18-SNAPSHOT"
+ },
+ "AVkWNYNYr4pSN7TrXcjY": Object {
+ "category": "OTHER",
+ "key": "AVkWNYNYr4pSN7TrXcjY",
+ "name": "foo"
+ },
+ "AVkWcQ8Hr4pSN7TrXcjZ": Object {
+ "category": "OTHER",
+ "key": "AVkWcQ8Hr4pSN7TrXcjZ",
+ "name": "custom"
+ }
+}
+`;
+
+exports[`test reducer 4`] = `
+Object {
+ "AVjUDBiSiXOcXjpycvde": Object {
+ "category": "VERSION",
+ "key": "AVjUDBiSiXOcXjpycvde",
+ "name": "2.18-SNAPSHOT"
+ },
+ "AVkWNYNYr4pSN7TrXcjY": Object {
+ "category": "OTHER",
+ "key": "AVkWNYNYr4pSN7TrXcjY",
+ "name": "foo"
+ },
+ "AVkWcQ8Hr4pSN7TrXcjZ": Object {
+ "category": "OTHER",
+ "key": "AVkWcQ8Hr4pSN7TrXcjZ",
+ "name": "new name"
+ }
+}
+`;
+
+exports[`test reducer 5`] = `
+Object {
+ "AVjUDBiSiXOcXjpycvde": Object {
+ "category": "VERSION",
+ "key": "AVjUDBiSiXOcXjpycvde",
+ "name": "2.18-SNAPSHOT"
+ },
+ "AVkWNYNYr4pSN7TrXcjY": Object {
+ "category": "OTHER",
+ "key": "AVkWNYNYr4pSN7TrXcjY",
+ "name": "foo"
+ }
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap
new file mode 100644
index 00000000000..6c9e8c74492
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/__snapshots__/paging-test.js.snap
@@ -0,0 +1,21 @@
+exports[`test reducer 1`] = `Object {}`;
+
+exports[`test reducer 2`] = `
+Object {
+ "project-foo": Object {
+ "pageIndex": 1,
+ "pageSize": 100,
+ "total": 3
+ }
+}
+`;
+
+exports[`test reducer 3`] = `
+Object {
+ "project-foo": Object {
+ "pageIndex": 2,
+ "pageSize": 30,
+ "total": 5
+ }
+}
+`;
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js
new file mode 100644
index 00000000000..f53081edd5d
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/analyses-test.js
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { configureTestStore } from '../../utils/configureStore';
+import analyses, { getAnalysis } from '../analyses';
+import { receiveProjectActivity, addEvent, deleteEvent, deleteAnalysis } from '../duck';
+
+const PROJECT = 'project-foo';
+
+const ANALYSES = [
+ {
+ key: 'AVgGkRvCrrTJiPpCD-rG',
+ date: '2016-10-27T16:33:50+0200',
+ events: [
+ {
+ key: 'AVjUDBiSiXOcXjpycvde',
+ category: 'VERSION',
+ name: '2.18-SNAPSHOT'
+ }
+ ]
+ },
+ {
+ key: 'AVgFqeOSKpGuA48ADATE',
+ date: '2016-10-27T12:21:15+0200',
+ events: []
+ },
+ {
+ key: 'AVgAgC1Vdo07z3PUnnkt',
+ date: '2016-10-26T12:17:29+0200',
+ events: [
+ {
+ key: 'AVkWNYNYr4pSN7TrXcjY',
+ category: 'OTHER',
+ name: 'foo'
+ }
+ ]
+ }
+];
+
+const PAGING = {
+ total: 3,
+ pageIndex: 1,
+ pageSize: 100
+};
+
+const NEW_EVENT = {
+ key: 'AVkWcQ8Hr4pSN7TrXcjZ',
+ category: 'OTHER',
+ name: 'custom'
+};
+
+it('reducer', () => {
+ const store = configureTestStore(analyses);
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(addEvent(ANALYSES[0].key, NEW_EVENT));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(deleteEvent(ANALYSES[0].key, NEW_EVENT.key));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(deleteAnalysis(PROJECT, ANALYSES[1].key));
+ expect(store.getState()).toMatchSnapshot();
+});
+
+it('selector `getAnalysis`', () => {
+ const analysis = ANALYSES[0];
+ const store = configureTestStore(analyses, { [analysis.key]: analysis });
+ expect(getAnalysis(store.getState(), analysis.key)).toBe(analysis);
+ expect(getAnalysis(store.getState(), 'random')).toBeFalsy();
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js
new file mode 100644
index 00000000000..c374065dd23
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/analysesByProject-test.js
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { configureTestStore } from '../../utils/configureStore';
+import analysesByProject from '../analysesByProject';
+import { receiveProjectActivity, deleteAnalysis } from '../duck';
+
+const PROJECT_FOO = 'project-foo';
+const PROJECT_BAR = 'project-bar';
+
+const ANALYSES_FOO = [
+ {
+ key: 'AVgFqeOSKpGuA48ADATE',
+ date: '2016-10-27T12:21:15+0200',
+ events: []
+ },
+ {
+ key: 'AVgAgC1Vdo07z3PUnnkt',
+ date: '2016-10-26T12:17:29+0200',
+ events: []
+ }
+];
+
+const ANALYSES_FOO_2 = [
+ {
+ key: 'AVgFqeOSKpGuA48ADATX',
+ date: '2016-10-27T12:21:15+0200',
+ events: []
+ }
+];
+
+const ANALYSES_BAR = [
+ {
+ key: 'AVgGkRvCrrTJiPpCD-rG',
+ date: '2016-10-27T16:33:50+0200',
+ events: []
+ }
+];
+
+const PAGING = {
+ total: 3,
+ pageIndex: 1,
+ pageSize: 100
+};
+
+it('reducer', () => {
+ const store = configureTestStore(analysesByProject);
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(receiveProjectActivity(PROJECT_FOO, ANALYSES_FOO, PAGING));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(receiveProjectActivity(PROJECT_FOO, ANALYSES_FOO_2, { pageIndex: 2 }));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(receiveProjectActivity(PROJECT_BAR, ANALYSES_BAR, PAGING));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(deleteAnalysis(PROJECT_FOO, 'AVgFqeOSKpGuA48ADATE'));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(deleteAnalysis(PROJECT_BAR, 'AVgGkRvCrrTJiPpCD-rG'));
+ expect(store.getState()).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js
new file mode 100644
index 00000000000..5a762840a07
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/duck-test.js
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { configureTestStore } from '../../utils/configureStore';
+import reducer, {
+ receiveProjectActivity,
+ getAnalyses,
+ getPaging,
+ addEvent,
+ changeEvent,
+ deleteEvent,
+ deleteAnalysis
+} from '../duck';
+
+const PROJECT = 'project-foo';
+
+const ANALYSES = [
+ {
+ key: 'AVgGkRvCrrTJiPpCD-rG',
+ date: '2016-10-27T16:33:50+0200',
+ events: [
+ {
+ key: 'AVjUDBiSiXOcXjpycvde',
+ category: 'VERSION',
+ name: '2.18-SNAPSHOT'
+ }
+ ]
+ },
+ {
+ key: 'AVgFqeOSKpGuA48ADATE',
+ date: '2016-10-27T12:21:15+0200',
+ events: []
+ },
+ {
+ key: 'AVgAgC1Vdo07z3PUnnkt',
+ date: '2016-10-26T12:17:29+0200',
+ events: [
+ {
+ key: 'AVkWNYNYr4pSN7TrXcjY',
+ category: 'OTHER',
+ name: 'foo'
+ }
+ ]
+ }
+];
+
+const PAGING = {
+ total: 3,
+ pageIndex: 1,
+ pageSize: 100
+};
+
+describe('actions', () => {
+ it('addEvent', () => {
+ expect(addEvent('foo', { key: 'bar' })).toMatchSnapshot();
+ });
+
+ it('changeEvent', () => {
+ expect(changeEvent('foo', { name: 'bar' })).toMatchSnapshot();
+ });
+
+ it('deleteEvent', () => {
+ expect(deleteEvent('foo', 'bar')).toMatchSnapshot();
+ });
+
+ it('deleteAnalysis', () => {
+ expect(deleteAnalysis('foo', 'bar')).toMatchSnapshot();
+ });
+});
+
+
+describe('selectors', () => {
+ it('getAnalyses', () => {
+ const store = configureTestStore(reducer);
+ store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING));
+ expect(getAnalyses(store.getState(), PROJECT)).toMatchSnapshot();
+ });
+
+ it('getPaging', () => {
+ const store = configureTestStore(reducer);
+ store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING));
+ expect(getPaging(store.getState(), PROJECT)).toMatchSnapshot();
+ });
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js
new file mode 100644
index 00000000000..fc95135a07a
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/events-test.js
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { configureTestStore } from '../../utils/configureStore';
+import events, { getEvent } from '../events';
+import { receiveProjectActivity, addEvent, changeEvent, deleteEvent } from '../duck';
+
+const PROJECT = 'project-foo';
+
+const ANALYSES = [
+ {
+ key: 'AVgGkRvCrrTJiPpCD-rG',
+ date: '2016-10-27T16:33:50+0200',
+ events: [
+ {
+ key: 'AVjUDBiSiXOcXjpycvde',
+ category: 'VERSION',
+ name: '2.18-SNAPSHOT'
+ }
+ ]
+ },
+ {
+ key: 'AVgFqeOSKpGuA48ADATE',
+ date: '2016-10-27T12:21:15+0200',
+ events: []
+ },
+ {
+ key: 'AVgAgC1Vdo07z3PUnnkt',
+ date: '2016-10-26T12:17:29+0200',
+ events: [
+ {
+ key: 'AVkWNYNYr4pSN7TrXcjY',
+ category: 'OTHER',
+ name: 'foo'
+ }
+ ]
+ }
+];
+
+const PAGING = {
+ total: 3,
+ pageIndex: 1,
+ pageSize: 100
+};
+
+const NEW_EVENT = {
+ key: 'AVkWcQ8Hr4pSN7TrXcjZ',
+ category: 'OTHER',
+ name: 'custom'
+};
+
+it('reducer', () => {
+ const store = configureTestStore(events);
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(addEvent(ANALYSES[0].key, NEW_EVENT));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(changeEvent(NEW_EVENT.key, { name: 'new name' }));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(deleteEvent(ANALYSES[0].key, NEW_EVENT.key));
+ expect(store.getState()).toMatchSnapshot();
+});
+
+it('selector `getEvent`', () => {
+ const event = ANALYSES[0].events[0];
+ const store = configureTestStore(events, { [event.key]: event });
+ expect(getEvent(store.getState(), event.key)).toBe(event);
+ expect(getEvent(store.getState(), 'random')).toBeFalsy();
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js b/server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js
new file mode 100644
index 00000000000..11f285a0f34
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/__tests__/paging-test.js
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { configureTestStore } from '../../utils/configureStore';
+import paging from '../paging';
+import { receiveProjectActivity } from '../duck';
+
+const PROJECT = 'project-foo';
+
+const ANALYSES = [];
+
+const PAGING_1 = {
+ total: 3,
+ pageIndex: 1,
+ pageSize: 100
+};
+
+const PAGING_2 = {
+ total: 5,
+ pageIndex: 2,
+ pageSize: 30
+};
+
+it('reducer', () => {
+ const store = configureTestStore(paging);
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING_1));
+ expect(store.getState()).toMatchSnapshot();
+
+ store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING_2));
+ expect(store.getState()).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/store/projectActivity/analyses.js b/server/sonar-web/src/main/js/store/projectActivity/analyses.js
new file mode 100644
index 00000000000..b60abff4b23
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/analyses.js
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import keyBy from 'lodash/keyBy';
+import type {
+ Action,
+ ReceiveProjectActivityAction,
+ AddEventAction,
+ DeleteEventAction,
+ DeleteAnalysisAction
+} from './duck';
+
+type Analysis = {
+ key: string;
+ date: string;
+ events: Array<string>
+};
+
+export type State = {
+ [key: string]: Analysis
+};
+
+const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => {
+ const analysesWithFlatEvents = action.analyses.map(analysis => ({
+ ...analysis,
+ events: analysis.events.map(event => event.key)
+ }));
+ return { ...state, ...keyBy(analysesWithFlatEvents, 'key') };
+};
+
+const addEvent = (state: State, action: AddEventAction): State => {
+ const analysis = state[action.analysis];
+ const newAnalysis = {
+ ...analysis,
+ events: [...analysis.events, action.event.key]
+ };
+ return { ...state, [action.analysis]: newAnalysis };
+};
+
+const deleteEvent = (state: State, action: DeleteEventAction): State => {
+ const analysis = state[action.analysis];
+ const newAnalysis = {
+ ...analysis,
+ events: analysis.events.filter(event => event !== action.event)
+ };
+ return { ...state, [action.analysis]: newAnalysis };
+};
+
+const deleteAnalysis = (state: State, action: DeleteAnalysisAction): State => {
+ const newState = { ...state };
+ delete newState[action.analysis];
+ return newState;
+};
+
+export default (state: State = {}, action: Action): State => {
+ switch (action.type) {
+ case 'RECEIVE_PROJECT_ACTIVITY':
+ return receiveProjectActivity(state, action);
+ case 'ADD_PROJECT_ACTIVITY_EVENT':
+ return addEvent(state, action);
+ case 'DELETE_PROJECT_ACTIVITY_EVENT':
+ return deleteEvent(state, action);
+ case 'DELETE_PROJECT_ACTIVITY_ANALYSIS':
+ return deleteAnalysis(state, action);
+ default:
+ return state;
+ }
+};
+
+export const getAnalysis = (state: State, key: string): Analysis => (
+ state[key]
+);
diff --git a/server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js b/server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js
new file mode 100644
index 00000000000..d5eb2fc7315
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/analysesByProject.js
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import type { Action, ReceiveProjectActivityAction, DeleteAnalysisAction } from './duck';
+
+export type State = {
+ [key: string]: Array<string>
+};
+
+const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => {
+ const analyses = state[action.project] || [];
+ const newAnalyses = action.analyses.map(analysis => analysis.key);
+ return {
+ ...state,
+ [action.project]: action.paging.pageIndex === 1 ? newAnalyses : [...analyses, ...newAnalyses]
+ };
+};
+
+const deleteAnalysis = (state: State, action: DeleteAnalysisAction): State => {
+ const analyses = state[action.project];
+ return {
+ ...state,
+ [action.project]: analyses.filter(key => key !== action.analysis)
+ };
+};
+
+export default (state: State = {}, action: Action): State => {
+ switch (action.type) {
+ case 'RECEIVE_PROJECT_ACTIVITY':
+ return receiveProjectActivity(state, action);
+ case 'DELETE_PROJECT_ACTIVITY_ANALYSIS':
+ return deleteAnalysis(state, action);
+ default:
+ return state;
+ }
+};
diff --git a/server/sonar-web/src/main/js/store/projectActivity/duck.js b/server/sonar-web/src/main/js/store/projectActivity/duck.js
new file mode 100644
index 00000000000..00ddb08b6d2
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/duck.js
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import { combineReducers } from 'redux';
+import analyses, * as fromAnalyses from './analyses';
+import type { State as AnalysesState } from './analyses';
+import analysesByProject from './analysesByProject';
+import type { State as AnalysesByProjectState } from './analysesByProject';
+import events, * as fromEvents from './events';
+import type { State as EventsState } from './events';
+import paging from './paging';
+import type { State as PagingState } from './paging';
+
+export type Event = {
+ key: string,
+ name: string;
+ category: string;
+ description?: string;
+};
+
+export type Analysis = {
+ key: string;
+ date: string;
+ events: Array<Event>
+};
+
+export type Paging = {
+ total: number,
+ pageIndex: number,
+ pageSize: number
+};
+
+export type ReceiveProjectActivityAction = {
+ type: 'RECEIVE_PROJECT_ACTIVITY',
+ project: string,
+ analyses: Array<Analysis>,
+ paging: Paging
+};
+
+export type AddEventAction = {
+ type: 'ADD_PROJECT_ACTIVITY_EVENT',
+ analysis: string,
+ event: Event
+};
+
+export type DeleteEventAction = {
+ type: 'DELETE_PROJECT_ACTIVITY_EVENT',
+ analysis: string,
+ event: string
+};
+
+export type ChangeEventAction = {
+ type: 'CHANGE_PROJECT_ACTIVITY_EVENT',
+ event: string,
+ changes: Object
+};
+
+export type DeleteAnalysisAction = {
+ type: 'DELETE_PROJECT_ACTIVITY_ANALYSIS',
+ project: string,
+ analysis: string
+};
+
+export type Action =
+ ReceiveProjectActivityAction |
+ AddEventAction |
+ DeleteEventAction |
+ ChangeEventAction |
+ DeleteAnalysisAction;
+
+export const receiveProjectActivity = (
+ project: string,
+ analyses: Array<Analysis>,
+ paging: Paging
+): ReceiveProjectActivityAction => ({
+ type: 'RECEIVE_PROJECT_ACTIVITY',
+ project,
+ analyses,
+ paging
+});
+
+export const addEvent = (analysis: string, event: Event): AddEventAction => ({
+ type: 'ADD_PROJECT_ACTIVITY_EVENT',
+ analysis,
+ event
+});
+
+export const deleteEvent = (analysis: string, event: string): DeleteEventAction => ({
+ type: 'DELETE_PROJECT_ACTIVITY_EVENT',
+ analysis,
+ event
+});
+
+export const changeEvent = (event: string, changes: Object): ChangeEventAction => ({
+ type: 'CHANGE_PROJECT_ACTIVITY_EVENT',
+ event,
+ changes
+});
+
+export const deleteAnalysis = (project: string, analysis: string): DeleteAnalysisAction => ({
+ type: 'DELETE_PROJECT_ACTIVITY_ANALYSIS',
+ project,
+ analysis
+});
+
+type State = {
+ analyses: AnalysesState,
+ analysesByProject: AnalysesByProjectState,
+ events: EventsState,
+ filter: string,
+ paging: PagingState,
+};
+
+export default combineReducers({ analyses, analysesByProject, events, paging });
+
+const getEvent = (state: State, key: string): Event => (
+ fromEvents.getEvent(state.events, key)
+);
+
+const getAnalysis = (state: State, key: string) => {
+ const analysis = fromAnalyses.getAnalysis(state.analyses, key);
+ const events: Array<Event> = analysis.events.map(key => getEvent(state, key));
+ return { ...analysis, events };
+};
+
+export const getAnalyses = (state: State, project: string) => (
+ state.analysesByProject[project] && state.analysesByProject[project].map(key => getAnalysis(state, key))
+);
+export const getPaging = (state: State, project: string) => (
+ state.paging[project]
+);
diff --git a/server/sonar-web/src/main/js/store/projectActivity/events.js b/server/sonar-web/src/main/js/store/projectActivity/events.js
new file mode 100644
index 00000000000..88e6fc27b26
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/events.js
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import keyBy from 'lodash/keyBy';
+import type {
+ Action,
+ ReceiveProjectActivityAction,
+ AddEventAction,
+ DeleteEventAction,
+ ChangeEventAction
+} from './duck';
+
+export type State = {
+ [key: string]: {
+ key: string,
+ name: string;
+ category: string;
+ description?: string;
+ }
+};
+
+const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => {
+ const events = {};
+ action.analyses.forEach(analysis => {
+ Object.assign(events, keyBy(analysis.events, 'key'));
+ });
+ return { ...state, ...events };
+};
+
+const addEvent = (state: State, action: AddEventAction): State => {
+ return { ...state, [action.event.key]: action.event };
+};
+
+const deleteEvent = (state: State, action: DeleteEventAction): State => {
+ const newState = { ...state };
+ delete newState[action.event];
+ return newState;
+};
+
+const changeEvent = (state: State, action: ChangeEventAction): State => {
+ const newEvent = { ...state[action.event], ...action.changes };
+ return { ...state, [action.event]: newEvent };
+};
+
+export default (state: State = {}, action: Action): State => {
+ switch (action.type) {
+ case 'RECEIVE_PROJECT_ACTIVITY':
+ return receiveProjectActivity(state, action);
+ case 'ADD_PROJECT_ACTIVITY_EVENT':
+ return addEvent(state, action);
+ case 'DELETE_PROJECT_ACTIVITY_EVENT':
+ return deleteEvent(state, action);
+ case 'CHANGE_PROJECT_ACTIVITY_EVENT':
+ return changeEvent(state, action);
+ default:
+ return state;
+ }
+};
+
+export const getEvent = (state: State, key: string) => (
+ state[key]
+);
diff --git a/server/sonar-web/src/main/js/store/projectActivity/paging.js b/server/sonar-web/src/main/js/store/projectActivity/paging.js
new file mode 100644
index 00000000000..46287a1183c
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/projectActivity/paging.js
@@ -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;
+};
+
diff --git a/server/sonar-web/src/main/js/store/rootActions.js b/server/sonar-web/src/main/js/store/rootActions.js
index 005c1158025..c6408588568 100644
--- a/server/sonar-web/src/main/js/store/rootActions.js
+++ b/server/sonar-web/src/main/js/store/rootActions.js
@@ -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)))
);
diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js
index 1a9e8793369..c7d223c78bf 100644
--- a/server/sonar-web/src/main/js/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/store/rootReducer.js
@@ -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)
);
diff --git a/server/sonar-web/src/main/js/store/utils/configureStore.js b/server/sonar-web/src/main/js/store/utils/configureStore.js
index 97397d836cd..361edd2dc18 100644
--- a/server/sonar-web/src/main/js/store/utils/configureStore.js
+++ b/server/sonar-web/src/main/js/store/utils/configureStore.js
@@ -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)
+);
diff --git a/server/sonar-web/src/main/less/components/modals.less b/server/sonar-web/src/main/less/components/modals.less
index 241364a6066..bc771a5d25b 100644
--- a/server/sonar-web/src/main/less/components/modals.less
+++ b/server/sonar-web/src/main/less/components/modals.less
@@ -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,
diff --git a/server/sonar-web/src/main/less/init/forms.less b/server/sonar-web/src/main/less/init/forms.less
index 4de11a1c3e8..43748629980 100644
--- a/server/sonar-web/src/main/less/init/forms.less
+++ b/server/sonar-web/src/main/less/init/forms.less
@@ -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;