]> source.dussan.org Git - sonarqube.git/commitdiff
Apply several feedbacks
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 7 Jul 2017 15:12:00 +0000 (17:12 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 13 Jul 2017 12:34:17 +0000 (14:34 +0200)
* Fix dates serializing
* Prevent clicks on event button to activate the graph tooltip
* Throttle part of the zoom graph
* Reload history when an analysis is deleted
* Fix multiple style flaws

15 files changed:
server/sonar-web/src/main/js/api/projectActivity.js
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/Event.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.js
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.js
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js
server/sonar-web/src/main/js/helpers/__tests__/query-test.js
server/sonar-web/src/main/js/helpers/query.js
server/sonar-web/src/main/less/components/graphics.less

index 8ffe600e8df73b145c1333c736f9b87f74256ac1..19dd91243dc21521750de9465679ace0a18582c4 100644 (file)
@@ -19,6 +19,7 @@
  */
 // @flow
 import { getJSON, postJSON, post } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
 
 type GetProjectActivityResponse = {
   analyses: Array<Object>,
@@ -38,7 +39,8 @@ type GetProjectActivityOptions = {
 
 export const getProjectActivity = (
   data: GetProjectActivityOptions
-): Promise<GetProjectActivityResponse> => getJSON('/api/project_analyses/search', data);
+): Promise<GetProjectActivityResponse> =>
+  getJSON('/api/project_analyses/search', data).catch(throwGlobalError);
 
 type CreateEventResponse = {
   analysis: string,
@@ -61,11 +63,11 @@ export const createEvent = (
   if (description) {
     data.description = description;
   }
-  return postJSON('/api/project_analyses/create_event', data).then(r => r.event);
+  return postJSON('/api/project_analyses/create_event', data).then(r => r.event, throwGlobalError);
 };
 
 export const deleteEvent = (event: string): Promise<*> =>
-  post('/api/project_analyses/delete_event', { event });
+  post('/api/project_analyses/delete_event', { event }).catch(throwGlobalError);
 
 export const changeEvent = (
   event: string,
@@ -79,8 +81,8 @@ export const changeEvent = (
   if (description) {
     data.description = description;
   }
-  return postJSON('/api/project_analyses/update_event', data).then(r => r.event);
+  return postJSON('/api/project_analyses/update_event', data).then(r => r.event, throwGlobalError);
 };
 
 export const deleteAnalysis = (analysis: string): Promise<*> =>
-  post('/api/project_analyses/delete', { analysis });
+  post('/api/project_analyses/delete', { analysis }).catch(throwGlobalError);
index 9746ea02c4a269a5cd81a15ff2deb364a9d9905b..05856e6601e4b57180bc0d13ea708ce0318dbe25 100644 (file)
@@ -22,7 +22,6 @@ import React from 'react';
 import { Link } from 'react-router';
 import Analysis from './Analysis';
 import PreviewGraph from './PreviewGraph';
-import throwGlobalError from '../../../app/utils/throwGlobalError';
 import { getMetrics } from '../../../api/metrics';
 import { getProjectActivity } from '../../../api/projectActivity';
 import { translate } from '../../../helpers/l10n';
@@ -72,7 +71,7 @@ export default class AnalysesList extends React.PureComponent {
       if (this.mounted) {
         this.setState({ analyses: response[0].analyses, metrics: response[1], loading: false });
       }
-    }, throwGlobalError);
+    });
   }
 
   renderList(analyses: Array<AnalysisType>) {
index d94a9f626c48285a85589dc961c4c830325e0719..c165f9740b83cc0f7662522cc2f7734c4ea724a1 100644 (file)
@@ -88,7 +88,8 @@ jest.mock('moment', () => date => ({
       valueOf: () => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
     };
   },
-  toDate: () => new Date(date)
+  toDate: () => new Date(date),
+  format: format => `Formated.${format}:${date.valueOf()}`
 }));
 
 describe('generateCoveredLinesMetric', () => {
@@ -164,11 +165,11 @@ describe('parseQuery', () => {
 describe('serializeQuery', () => {
   it('should serialize query for api request', () => {
     expect(utils.serializeQuery(QUERY)).toEqual({
-      from: '2017-04-27T06:21:32.000Z',
+      from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000',
       project: 'foo'
     });
     expect(utils.serializeQuery({ ...QUERY, graph: 'coverage', category: 'test' })).toEqual({
-      from: '2017-04-27T06:21:32.000Z',
+      from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000',
       project: 'foo',
       category: 'test'
     });
@@ -178,14 +179,14 @@ describe('serializeQuery', () => {
 describe('serializeUrlQuery', () => {
   it('should serialize query for url', () => {
     expect(utils.serializeUrlQuery(QUERY)).toEqual({
-      from: '2017-04-27T06:21:32.000Z',
+      from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000',
       id: 'foo',
       custom_metrics: 'foo,bar,baz'
     });
     expect(
       utils.serializeUrlQuery({ ...QUERY, graph: 'coverage', category: 'test', customMetrics: [] })
     ).toEqual({
-      from: '2017-04-27T06:21:32.000Z',
+      from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000',
       id: 'foo',
       graph: 'coverage',
       category: 'test'
index 6e1a3878c88a5dbf85a1b2afe92128b1dbca4dee..f5bc0579b16c5cd204f0c9359009f9cc15e28f40 100644 (file)
@@ -56,7 +56,8 @@ export default class Event extends React.PureComponent {
     this.mounted = false;
   }
 
-  startChanging = () => {
+  startChanging = (e: MouseEvent) => {
+    e.stopPropagation();
     this.setState({ changing: true });
   };
 
@@ -66,7 +67,8 @@ export default class Event extends React.PureComponent {
     }
   };
 
-  startDeleting = () => {
+  startDeleting = (e: MouseEvent) => {
+    e.stopPropagation();
     this.setState({ deleting: true });
   };
 
index 9c98102a95e93f1de07b50e060ca82f88dae65dc..a0a8a41dd318c5dea54af56d081b4b3b9e098590 100644 (file)
@@ -118,6 +118,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent {
       }
     }
   };
+
   handleScroll = () => this.updateStickyBadges(true);
 
   resetScrollTop = (newScrollTop: number, forceBadgeAlignement?: boolean) => {
index 842b3ff8a4472c27af7cbf358349df24c858247b..72490f7d41bc1b1b3fdaca3584a4d79b46ca7636 100644 (file)
@@ -45,6 +45,8 @@ export default class ProjectActivityAnalysis extends React.PureComponent {
 
   handleClick = () => this.props.updateSelectedDate(this.props.analysis.date);
 
+  stopPropagation = (e: Event) => e.stopPropagation();
+
   render() {
     const { analysis, isFirst, canAdmin } = this.props;
     const { date, events } = analysis;
@@ -69,7 +71,8 @@ export default class ProjectActivityAnalysis extends React.PureComponent {
             <div className="dropdown display-inline-block">
               <button
                 className="js-analysis-actions button-small button-compact dropdown-toggle"
-                data-toggle="dropdown">
+                data-toggle="dropdown"
+                onClick={this.stopPropagation}>
                 <i className="icon-settings" />
                 {' '}
                 <i className="icon-dropdown" />
index afe28a6ebe1fbfc4fc9f68bc58c98e98878704fe..dd3b3690c07ce559922a0fee2eaa3f4a246de988 100644 (file)
@@ -118,8 +118,7 @@ class ProjectActivityAppContainer extends React.PureComponent {
       .createEvent(analysis, name, category)
       .then(
         ({ analysis, ...event }) =>
-          this.mounted && this.setState(actions.addCustomEvent(analysis, event)),
-        throwGlobalError
+          this.mounted && this.setState(actions.addCustomEvent(analysis, event))
       );
 
   addVersion = (analysis: string, version: string): Promise<*> =>
@@ -130,25 +129,21 @@ class ProjectActivityAppContainer extends React.PureComponent {
       .changeEvent(event, name)
       .then(
         ({ analysis, ...event }) =>
-          this.mounted && this.setState(actions.changeEvent(analysis, event)),
-        throwGlobalError
+          this.mounted && this.setState(actions.changeEvent(analysis, event))
       );
 
   deleteAnalysis = (analysis: string): Promise<*> =>
-    api
-      .deleteAnalysis(analysis)
-      .then(
-        () => this.mounted && this.setState(actions.deleteAnalysis(analysis)),
-        throwGlobalError
-      );
+    api.deleteAnalysis(analysis).then(() => {
+      if (this.mounted) {
+        this.updateGraphData(this.state.query.graph, this.state.query.customMetrics);
+        this.setState(actions.deleteAnalysis(analysis));
+      }
+    });
 
   deleteEvent = (analysis: string, event: string): Promise<*> =>
     api
       .deleteEvent(event)
-      .then(
-        () => this.mounted && this.setState(actions.deleteEvent(analysis, event)),
-        throwGlobalError
-      );
+      .then(() => this.mounted && this.setState(actions.deleteEvent(analysis, event)));
 
   fetchActivity = (
     project: string,
@@ -159,13 +154,12 @@ class ProjectActivityAppContainer extends React.PureComponent {
     }
   ): Promise<{ analyses: Array<Analysis>, paging: Paging }> => {
     const parameters = { project, p, ps };
-    return api.getProjectActivity({ ...parameters, ...additional }).then(
-      ({ analyses, paging }) => ({
+    return api
+      .getProjectActivity({ ...parameters, ...additional })
+      .then(({ analyses, paging }) => ({
         analyses: analyses.map(analysis => ({ ...analysis, date: moment(analysis.date).toDate() })),
         paging
-      }),
-      throwGlobalError
-    );
+      }));
   };
 
   fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> => {
index 007b04f4d52b37c618264c84110da29364591efc..d4e82b0fa622187e9ce9934ccecbb635e6300513 100644 (file)
@@ -51,7 +51,7 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent {
     return (
       <header className="page-header">
         <Select
-          className="input-medium"
+          className="pull-left input-medium"
           clearable={false}
           searchable={false}
           value={this.props.graph}
@@ -61,7 +61,7 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent {
         {isCustomGraph(this.props.graph) &&
           <AddGraphMetric
             addMetric={this.props.addCustomMetric}
-            className="spacer-left"
+            className="pull-left spacer-left"
             metrics={this.props.metrics}
             selectedMetrics={this.props.selectedMetrics}
           />}
index 92c4436f20d9b64d987fa881e114a06f64272a5f..028fbc125a145603cd83f7c24fecdb3f2cf3d66e 100644 (file)
@@ -52,8 +52,9 @@ export default class AddEventForm extends React.PureComponent {
     this.mounted = false;
   }
 
-  openForm = (e: Object) => {
+  openForm = (e: Event) => {
     e.preventDefault();
+    e.stopPropagation();
     if (this.mounted) {
       this.setState({ open: true });
     }
index 138274caefc5273e33f1a1ff017b323cb99263ba..d96a05f73346500f7abe6e31bf7a1b4ba0b83102 100644 (file)
@@ -49,8 +49,9 @@ export default class RemoveAnalysisForm extends React.PureComponent {
     this.mounted = false;
   }
 
-  openForm = (evt: Event) => {
-    evt.preventDefault();
+  openForm = (e: Event) => {
+    e.preventDefault();
+    e.stopPropagation();
     if (this.mounted) {
       this.setState({ open: true });
     }
@@ -74,7 +75,7 @@ export default class RemoveAnalysisForm extends React.PureComponent {
     }
   };
 
-  handleSubmit = (e: Object) => {
+  handleSubmit = (e: Event) => {
     e.preventDefault();
     this.setState({ processing: true });
     this.props
@@ -104,7 +105,9 @@ export default class RemoveAnalysisForm extends React.PureComponent {
             {this.state.processing
               ? <i className="spinner" />
               : <div>
-                  <button type="submit" className="button-red">{translate('delete')}</button>
+                  <button type="submit" className="button-red" autoFocus={true}>
+                    {translate('delete')}
+                  </button>
                   <button type="reset" className="button-link" onClick={this.closeForm}>
                     {translate('cancel')}
                   </button>
index cc0b6d1bd7ad90edda695ecfe9c9d17ef30de537..740aef24ba3dd773db860e9f7dd64cdfcc8bdff5 100644 (file)
@@ -54,6 +54,7 @@
 
 .project-activity-graph {
   flex: 1;
+  overflow: hidden;
 }
 
 .project-activity-graph-legends {
index 4de2cda1f772e06c24c2e2d6b4bdfb0835753afc..81ad1921a0e5c714271180c8f4ac969335d19119 100644 (file)
@@ -20,7 +20,7 @@
 // @flow
 import React from 'react';
 import classNames from 'classnames';
-import { flatten, sortBy } from 'lodash';
+import { flatten, sortBy, throttle } from 'lodash';
 import { extent, max } from 'd3-array';
 import { scaleLinear, scalePoint, scaleTime } from 'd3-scale';
 import { line as d3Line, area, curveBasis } from 'd3-shape';
@@ -51,15 +51,18 @@ type State = {
 
 export default class ZoomTimeLine extends React.PureComponent {
   props: Props;
+  state: State;
+
   static defaultProps = {
     padding: [0, 0, 18, 0],
     showXTicks: true
   };
 
-  state: State = {
-    overlayLeftPos: null,
-    newZoomStart: null
-  };
+  constructor(props: Props) {
+    super(props);
+    this.state = { overlayLeftPos: null, newZoomStart: null };
+    this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40);
+  }
 
   getRatingScale = (availableHeight: number) =>
     scalePoint().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]);
@@ -135,10 +138,10 @@ export default class ZoomTimeLine extends React.PureComponent {
   handleNewZoomDrag = (xScale: Scale, xDim: Array<number>) => (e: Event, data: DraggableData) => {
     const { newZoomStart, overlayLeftPos } = this.state;
     if (newZoomStart != null && overlayLeftPos != null && data.deltaX) {
-      this.handleZoomUpdate(xScale, [
-        newZoomStart,
-        Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1]))
-      ]);
+      this.handleZoomUpdate(
+        xScale,
+        sortBy([newZoomStart, Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1]))])
+      );
     }
   };
 
@@ -149,7 +152,7 @@ export default class ZoomTimeLine extends React.PureComponent {
     const { newZoomStart, overlayLeftPos } = this.state;
     if (newZoomStart != null && overlayLeftPos != null) {
       const x = Math.round(Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1])));
-      this.handleZoomUpdate(xScale, newZoomStart === x ? xDim : [newZoomStart, x]);
+      this.handleZoomUpdate(xScale, newZoomStart === x ? xDim : sortBy([newZoomStart, x]));
       this.setState({ newZoomStart: null, overlayLeftPos: null });
     }
   };
@@ -303,7 +306,10 @@ export default class ZoomTimeLine extends React.PureComponent {
     const endX = Math.round(this.props.endDate ? xScale(this.props.endDate) : xDim[1]);
     const xArray = sortBy([startX, endX]);
     const zoomBoxWidth = xArray[1] - xArray[0];
-    const showZoomArea = this.state.newZoomStart == null || this.state.newZoomStart === startX;
+    const showZoomArea =
+      this.state.newZoomStart == null ||
+      this.state.newZoomStart === startX ||
+      this.state.newZoomStart === endX;
 
     return (
       <g className="chart-zoom">
index 6ef12860f005df891e8a2b9ecc12eeb5494646e5..982f9375a362e1a009cbb839204ffd6957305ec1 100644 (file)
@@ -17,6 +17,7 @@
  * 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 * as query from '../query';
 
 describe('queriesEqual', () => {
@@ -78,10 +79,9 @@ describe('parseAsDate', () => {
 });
 
 describe('serializeDate', () => {
+  const date = moment.utc('2016-06-20T13:09:48.256Z');
   it('should serialize string correctly', () => {
-    expect(query.serializeDate(new Date('2016-06-20T13:09:48.256Z'))).toBe(
-      '2016-06-20T13:09:48.256Z'
-    );
+    expect(query.serializeDate(date)).toBe('2016-06-20T13:09:48+0000');
     expect(query.serializeDate('')).toBeUndefined();
     expect(query.serializeDate()).toBeUndefined();
   });
index 37663f2cd49522cab486c3fc4288484f83cf9f31..5f4a28ec12cb73b91b10b0671cd1e70992cd559b 100644 (file)
@@ -72,7 +72,7 @@ export const parseAsArray = <T>(value: ?string, itemParser: string => T): Array<
 
 export const serializeDate = (value: ?Date): ?string => {
   if (value != null && value.toISOString) {
-    return value.toISOString();
+    return moment(value).format('YYYY-MM-DDTHH:mm:ssZZ');
   }
 };
 
index 14f9d2d1e0e01a2e78142960ab89aa4bf5e2bec8..5ba0e6da818a18ea19b1e89622211cc99c49e3bd 100644 (file)
   fill: @secondFontColor;
   font-size: 10px;
   text-anchor: middle;
+  user-select: none;
 }
 
 .chart-mouse-events-overlay {