diff options
Diffstat (limited to 'server')
8 files changed, 218 insertions, 2 deletions
diff --git a/server/sonar-web/src/main/js/api/events.js b/server/sonar-web/src/main/js/api/events.js index 11852bef775..23c15b9c9ee 100644 --- a/server/sonar-web/src/main/js/api/events.js +++ b/server/sonar-web/src/main/js/api/events.js @@ -1,7 +1,17 @@ import { getJSON } from '../helpers/request.js'; + +/** + * Return events for a component + * @param {string} componentKey + * @param {string} [categories] + * @returns {Promise} + */ export function getEvents (componentKey, categories) { let url = baseUrl + '/api/events'; - let data = { resource: componentKey, categories }; + let data = { resource: componentKey }; + if (categories) { + data.categories = categories; + } return getJSON(url, data); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/event.js b/server/sonar-web/src/main/js/apps/overview/components/event.js new file mode 100644 index 00000000000..fb29f3d3cfd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/event.js @@ -0,0 +1,31 @@ +import React from 'react'; +import moment from 'moment'; + +import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin'; + + +export const Event = React.createClass({ + mixins: [TooltipsMixin], + + propTypes: { + event: React.PropTypes.shape({ + id: React.PropTypes.string.isRequired, + date: React.PropTypes.object.isRequired, + type: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + text: React.PropTypes.string + }) + }, + + render () { + const { event } = this.props; + return <li className="spacer-top"> + <p data-toggle="tooltip" title={event.text}> + <strong className="js-event-type">{window.t('event.category', event.type)}</strong> + : + <span className="js-event-name">{event.name}</span> + </p> + <p className="note little-spacer-top js-event-date">{moment(event.date).format('LL')}</p> + </li>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/components/events-list-filter.js b/server/sonar-web/src/main/js/apps/overview/components/events-list-filter.js new file mode 100644 index 00000000000..03139675b0a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/events-list-filter.js @@ -0,0 +1,22 @@ +import React from 'react'; + + +const TYPES = ['All', 'Version', 'Alert', 'Profile', 'Other']; + + +export const EventsListFilter = React.createClass({ + propTypes: { + onFilter: React.PropTypes.func.isRequired, + currentFilter: React.PropTypes.string.isRequired + }, + + handleChange() { + const value = this.refs.select.value; + this.props.onFilter(value); + }, + + render () { + const options = TYPES.map(type => <option key={type} value={type}>{window.t('event.category', type)}</option>); + return <select ref="select" onChange={this.handleChange} value={this.props.currentFilter}>{options}</select>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/components/events-list.js b/server/sonar-web/src/main/js/apps/overview/components/events-list.js new file mode 100644 index 00000000000..5f9f6786665 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/events-list.js @@ -0,0 +1,79 @@ +import React from 'react'; + +import { Event } from './event'; +import { EventsListFilter } from './events-list-filter'; + + +const LIMIT = 5; + + +export const EventsList = React.createClass({ + propTypes: { + events: React.PropTypes.arrayOf(React.PropTypes.shape({ + id: React.PropTypes.string.isRequired, + date: React.PropTypes.object.isRequired, + type: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + text: React.PropTypes.string + }).isRequired).isRequired + }, + + getInitialState() { + return { limited: true, filter: 'All' }; + }, + + 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(filteredEvents) { + if (filteredEvents.length > LIMIT) { + const text = this.state.limited ? window.t('widget.events.show_all') : window.t('hide'); + return <p className="spacer-top note"> + <a onClick={this.handleClick} href="#">{text}</a> + </p>; + } else { + return null; + } + }, + + renderList (events) { + if (events.length) { + return <ul>{events.map(event => <Event key={event.id} event={event}/>)}</ul>; + } else { + return <p className="spacer-top note">{window.t('no_results')}</p>; + } + }, + + render () { + const filteredEvents = this.filterEvents(this.props.events); + const events = this.limitEvents(filteredEvents); + return <div className="overview-meta-card"> + <div className="clearfix"> + <h4 className="pull-left overview-meta-header">{window.t('widget.events.name')}</h4> + <div className="pull-right"> + <EventsListFilter currentFilter={this.state.filter} onFilter={this.handleFilter}/> + </div> + </div> + {this.renderList(events)} + {this.renderMoreLink(filteredEvents)} + </div>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta.js b/server/sonar-web/src/main/js/apps/overview/meta.js index dea84f07899..8228db26f34 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta.js @@ -1,9 +1,33 @@ import _ from 'underscore'; +import moment from 'moment'; import React from 'react'; + import { QualityProfileLink } from './../../components/shared/quality-profile-link'; import { QualityGateLink } from './../../components/shared/quality-gate-link'; +import { getEvents } from '../../api/events'; +import { EventsList } from './components/events-list'; + export default React.createClass({ + componentDidMount() { + this.requestEvents(); + }, + + requestEvents () { + return getEvents(this.props.component.key).then(events => { + 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 }); + }); + }, + isView() { return this.props.component.qualifier === 'VW' || this.props.component.qualifier === 'SVW'; }, @@ -12,6 +36,14 @@ export default React.createClass({ return this.props.component.qualifier === 'DEV'; }, + renderEvents() { + if (this.state && this.state.events) { + return <EventsList component={this.props.component} events={this.state.events}/>; + } else { + return null; + } + }, + render() { let profiles = (this.props.component.profiles || []).map(profile => { return ( @@ -71,6 +103,7 @@ export default React.createClass({ {linksCard} {gateCard} {profilesCard} + {this.renderEvents()} </div> ); } diff --git a/server/sonar-web/src/main/less/mixins.less b/server/sonar-web/src/main/less/mixins.less index a19d96474b2..8785c543464 100644 --- a/server/sonar-web/src/main/less/mixins.less +++ b/server/sonar-web/src/main/less/mixins.less @@ -1,6 +1,6 @@ @import (reference) "variables"; -.clearfix() { +.clearfix { &:before, &:after { display: table; content: ""; line-height: 0; } &:after { clear: both; } } diff --git a/server/sonar-web/tests/apps/overview/components/event-test.js b/server/sonar-web/tests/apps/overview/components/event-test.js new file mode 100644 index 00000000000..281898c6728 --- /dev/null +++ b/server/sonar-web/tests/apps/overview/components/event-test.js @@ -0,0 +1,23 @@ +import { expect } from 'chai'; +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; + +import { Event } from '../../../../src/main/js/apps/overview/components/event'; + + +describe('Overview :: Event', function () { + it('should render event', function () { + let output = TestUtils.renderIntoDocument( + <Event event={{ id: '1', name: '1.5', type: 'Version', date: new Date(2015, 0, 1) }}/>); + expect( + findDOMNode(TestUtils.findRenderedDOMComponentWithClass(output, 'js-event-date')).textContent + ).to.include('2015'); + expect( + findDOMNode(TestUtils.findRenderedDOMComponentWithClass(output, 'js-event-name')).textContent + ).to.include('1.5'); + expect( + findDOMNode(TestUtils.findRenderedDOMComponentWithClass(output, 'js-event-type')).textContent + ).to.include('Version'); + }); +}); diff --git a/server/sonar-web/tests/apps/overview/components/events-list-filter-test.js b/server/sonar-web/tests/apps/overview/components/events-list-filter-test.js new file mode 100644 index 00000000000..9c5853cd348 --- /dev/null +++ b/server/sonar-web/tests/apps/overview/components/events-list-filter-test.js @@ -0,0 +1,18 @@ +import { expect } from 'chai'; +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; +import sinon from 'sinon'; + +import { EventsListFilter } from '../../../../src/main/js/apps/overview/components/events-list-filter'; + + +describe('Overview :: EventsListFilter', function () { + it('should render options', function () { + let spy = sinon.spy(); + let output = TestUtils.renderIntoDocument( + <EventsListFilter onFilter={spy} currentFilter="All"/>); + let options = TestUtils.scryRenderedDOMComponentsWithTag(output, 'option'); + expect(options).to.have.length(5); + }); +}); |