From 6e07fc504ca021d21c54f504fc1a815bd76dc0c8 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 1 Dec 2015 14:29:02 +0100 Subject: [PATCH] SONAR-7063 Show events history in the project overview page --- server/sonar-web/src/main/js/api/events.js | 12 ++- .../main/js/apps/overview/components/event.js | 31 ++++++++ .../overview/components/events-list-filter.js | 22 ++++++ .../apps/overview/components/events-list.js | 79 +++++++++++++++++++ .../src/main/js/apps/overview/meta.js | 33 ++++++++ server/sonar-web/src/main/less/mixins.less | 2 +- .../apps/overview/components/event-test.js | 23 ++++++ .../components/events-list-filter-test.js | 18 +++++ .../resources/org/sonar/l10n/core.properties | 1 + 9 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/overview/components/event.js create mode 100644 server/sonar-web/src/main/js/apps/overview/components/events-list-filter.js create mode 100644 server/sonar-web/src/main/js/apps/overview/components/events-list.js create mode 100644 server/sonar-web/tests/apps/overview/components/event-test.js create mode 100644 server/sonar-web/tests/apps/overview/components/events-list-filter-test.js 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
  • +

    + {window.t('event.category', event.type)} + :  + {event.name} +

    +

    {moment(event.date).format('LL')}

    +
  • ; + } +}); 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 => ); + return ; + } +}); 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

    + {text} +

    ; + } else { + return null; + } + }, + + renderList (events) { + if (events.length) { + return ; + } else { + return

    {window.t('no_results')}

    ; + } + }, + + render () { + const filteredEvents = this.filterEvents(this.props.events); + const events = this.limitEvents(filteredEvents); + return
    +
    +

    {window.t('widget.events.name')}

    +
    + +
    +
    + {this.renderList(events)} + {this.renderMoreLink(filteredEvents)} +
    ; + } +}); 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 ; + } 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()} ); } 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( + ); + 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( + ); + let options = TestUtils.scryRenderedDOMComponentsWithTag(output, 'option'); + expect(options).to.have.length(5); + }); +}); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 9dca2e81f50..7ad87bab250 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -416,6 +416,7 @@ project_links.scm_dev=Developer connection # #------------------------------------------------------------------------------ +event.category.All=All event.category.Version=Version event.category.Alert=Quality Gate event.category.Profile=Quality Profile -- 2.39.5