diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-07 17:12:00 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-13 14:34:17 +0200 |
commit | b5e582ccc44788e5d57dde7df2ed0b0050f82c1a (patch) | |
tree | 8dae37a74e5f9e9569d282b93a212a4e298499e6 /server/sonar-web | |
parent | 598a5a81e4842fa03705f7f1c26a79bc520c70ef (diff) | |
download | sonarqube-b5e582ccc44788e5d57dde7df2ed0b0050f82c1a.tar.gz sonarqube-b5e582ccc44788e5d57dde7df2ed0b0050f82c1a.zip |
Apply several feedbacks
* 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
Diffstat (limited to 'server/sonar-web')
15 files changed, 70 insertions, 56 deletions
diff --git a/server/sonar-web/src/main/js/api/projectActivity.js b/server/sonar-web/src/main/js/api/projectActivity.js index 8ffe600e8df..19dd91243dc 100644 --- a/server/sonar-web/src/main/js/api/projectActivity.js +++ b/server/sonar-web/src/main/js/api/projectActivity.js @@ -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); 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 index 9746ea02c4a..05856e6601e 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js +++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js @@ -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>) { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js index d94a9f626c4..c165f9740b8 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js @@ -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' 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 index 6e1a3878c88..f5bc0579b16 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.js @@ -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 }); }; 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 index 9c98102a95e..a0a8a41dd31 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js @@ -118,6 +118,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent { } } }; + handleScroll = () => this.updateStickyBadges(true); resetScrollTop = (newScrollTop: number, forceBadgeAlignement?: boolean) => { 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 index 842b3ff8a44..72490f7d41b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js @@ -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" /> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index afe28a6ebe1..dd3b3690c07 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -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>> => { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js index 007b04f4d52..d4e82b0fa62 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js @@ -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} />} 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 index 92c4436f20d..028fbc125a1 100644 --- 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 @@ -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 }); } 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 index 138274caefc..d96a05f7334 100644 --- 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 @@ -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> 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 index cc0b6d1bd7a..740aef24ba3 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css @@ -54,6 +54,7 @@ .project-activity-graph { flex: 1; + overflow: hidden; } .project-activity-graph-legends { diff --git a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js index 4de2cda1f77..81ad1921a0e 100644 --- a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js +++ b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js @@ -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"> diff --git a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js index 6ef12860f00..982f9375a36 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/helpers/query.js b/server/sonar-web/src/main/js/helpers/query.js index 37663f2cd49..5f4a28ec12c 100644 --- a/server/sonar-web/src/main/js/helpers/query.js +++ b/server/sonar-web/src/main/js/helpers/query.js @@ -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'); } }; diff --git a/server/sonar-web/src/main/less/components/graphics.less b/server/sonar-web/src/main/less/components/graphics.less index 14f9d2d1e0e..5ba0e6da818 100644 --- a/server/sonar-web/src/main/less/components/graphics.less +++ b/server/sonar-web/src/main/less/components/graphics.less @@ -294,6 +294,7 @@ fill: @secondFontColor; font-size: 10px; text-anchor: middle; + user-select: none; } .chart-mouse-events-overlay { |