]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7063 Show events history in the project overview page
authorStas Vilchik <vilchiks@gmail.com>
Tue, 1 Dec 2015 13:29:02 +0000 (14:29 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Tue, 1 Dec 2015 13:29:02 +0000 (14:29 +0100)
server/sonar-web/src/main/js/api/events.js
server/sonar-web/src/main/js/apps/overview/components/event.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/events-list-filter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/events-list.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta.js
server/sonar-web/src/main/less/mixins.less
server/sonar-web/tests/apps/overview/components/event-test.js [new file with mode: 0644]
server/sonar-web/tests/apps/overview/components/events-list-filter-test.js [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 11852bef7756b93594081936663992abca75cf5c..23c15b9c9ee7351ea3ab1991029089b5fd187443 100644 (file)
@@ -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 (file)
index 0000000..fb29f3d
--- /dev/null
@@ -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>
+        :&nbsp;
+        <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 (file)
index 0000000..0313967
--- /dev/null
@@ -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 (file)
index 0000000..5f9f678
--- /dev/null
@@ -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>;
+  }
+});
index dea84f078995a927106db368ede0cd249b30f32a..8228db26f3447ee48c2185962618aaaa65926f2e 100644 (file)
@@ -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>
     );
   }
index a19d96474b252fd266cc5b221d00bbccc47f59ec..8785c543464d63b9045fd53fdd956e180ded496a 100644 (file)
@@ -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 (file)
index 0000000..281898c
--- /dev/null
@@ -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 (file)
index 0000000..9c5853c
--- /dev/null
@@ -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);
+  });
+});
index 9dca2e81f50b065775952bb9ebefa4876b60c620..7ad87bab250a2bb63ae85b0d418d92dd6f0b7e41 100644 (file)
@@ -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