diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-12-19 14:07:32 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-12-19 14:07:32 +0100 |
commit | 2a91ab92dac4203e4410f5b2ab0ffbd1a6efac0e (patch) | |
tree | c711f5da56f2f4ef0dd90f897112372223ece703 /server/sonar-web/src | |
parent | afb6610a84b5f237bd036c900dd1a20ca8526311 (diff) | |
download | sonarqube-2a91ab92dac4203e4410f5b2ab0ffbd1a6efac0e.tar.gz sonarqube-2a91ab92dac4203e4410f5b2ab0ffbd1a6efac0e.zip |
SONAR-7674 Add Activity Stream interface (#1459)
Diffstat (limited to 'server/sonar-web/src')
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; |