import ProjectActivityAnalysis from './ProjectActivityAnalysis';
import FormattedDate from '../../../components/ui/FormattedDate';
import { translate } from '../../../helpers/l10n';
-import { activityQueryChanged, getAnalysesByVersionByDay } from '../utils';
+import {
+ activityQueryChanged,
+ getAnalysesByVersionByDay,
+ selectedDateQueryChanged
+} from '../utils';
+import type { RawQuery } from '../../../helpers/query';
import type { Analysis, Query } from '../types';
type Props = {
deleteAnalysis: (analysis: string) => Promise<*>,
deleteEvent: (analysis: string, event: string) => Promise<*>,
loading: boolean,
- query: Query
+ query: Query,
+ updateQuery: RawQuery => void
};
export default class ProjectActivityAnalysesList extends React.PureComponent {
- scrollContainer: HTMLElement;
+ analyses: HTMLCollection<HTMLElement>;
badges: HTMLCollection<HTMLElement>;
props: Props;
+ scrollContainer: HTMLElement;
constructor(props: Props) {
super(props);
componentDidMount() {
this.badges = document.getElementsByClassName('project-activity-version-badge');
+ this.analyses = document.getElementsByClassName('project-activity-analysis');
}
componentDidUpdate(prevProps: Props) {
- if (prevProps.analysis !== this.props.analyses && this.scrollContainer) {
- if (activityQueryChanged(prevProps.query, this.props.query)) {
- this.scrollContainer.scrollTop = 0;
- }
- for (let i = 1; i < this.badges.length; i++) {
- this.badges[i].removeAttribute('originOffsetTop');
- this.badges[i].classList.remove('sticky');
+ if (this.scrollContainer) {
+ const selectedDateChanged = selectedDateQueryChanged(prevProps.query, this.props.query);
+ if (selectedDateChanged || prevProps.analysis !== this.props.analyses) {
+ if (selectedDateChanged && this.props.query.selectedDate) {
+ const selectedDate = this.props.query.selectedDate.valueOf();
+ for (let i = 1; i < this.analyses.length; i++) {
+ if (Number(this.analyses[i].getAttribute('data-date')) === selectedDate) {
+ const containerHeight = this.scrollContainer.offsetHeight - 100;
+ const scrollDiff = Math.abs(
+ this.scrollContainer.scrollTop - this.analyses[i].offsetTop
+ );
+ // Center only the extremities and the ones outside of the container
+ if (scrollDiff > containerHeight || scrollDiff < 100) {
+ this.resetScrollTop(this.analyses[i].offsetTop - containerHeight / 2);
+ }
+ break;
+ }
+ }
+ } else if (activityQueryChanged(prevProps.query, this.props.query)) {
+ this.resetScrollTop(0, true);
+ }
}
- this.handleScroll();
}
}
- handleScroll = () => {
+ updateStickyBadges = (forceBadgeAlignement?: boolean) => {
if (this.scrollContainer && this.badges) {
const scrollTop = this.scrollContainer.scrollTop;
if (scrollTop != null) {
const badge = this.badges[i];
let originOffsetTop = badge.getAttribute('originOffsetTop');
if (originOffsetTop == null) {
+ // Set the originOffsetTop attribute, to avoid using getBoundingClientRect
originOffsetTop = badge.offsetTop;
badge.setAttribute('originOffsetTop', originOffsetTop.toString());
}
if (Number(originOffsetTop) < scrollTop + 18 + i * 2) {
- if (!badge.classList.contains('sticky')) {
+ if (forceBadgeAlignement && !badge.classList.contains('sticky')) {
newScrollTop = originOffsetTop;
}
badge.classList.add('sticky');
badge.classList.remove('sticky');
}
}
- if (newScrollTop != null) {
+ if (forceBadgeAlignement && newScrollTop != null) {
this.scrollContainer.scrollTop = newScrollTop - 6;
}
}
}
};
+ handleScroll = () => this.updateStickyBadges(true);
+
+ resetScrollTop = (newScrollTop: number, forceBadgeAlignement?: boolean) => {
+ this.scrollContainer.scrollTop = newScrollTop;
+ for (let i = 1; i < this.badges.length; i++) {
+ this.badges[i].removeAttribute('originOffsetTop');
+ this.badges[i].classList.remove('sticky');
+ }
+ this.updateStickyBadges(forceBadgeAlignement);
+ };
+
+ updateSelectedDate = (date: Date) => {
+ this.props.updateQuery({ selectedDate: date });
+ };
render() {
if (this.props.analyses.length === 0) {
const firstAnalysisKey = this.props.analyses[0].key;
const byVersionByDay = getAnalysesByVersionByDay(this.props.analyses);
+ const selectedDate = this.props.query.selectedDate
+ ? this.props.query.selectedDate.valueOf()
+ : null;
return (
<ul
className={classNames('project-activity-versions-list', this.props.className)}
deleteEvent={this.props.deleteEvent}
isFirst={analysis.key === firstAnalysisKey}
key={analysis.key}
+ selected={analysis.date.valueOf() === selectedDate}
+ updateSelectedDate={this.updateSelectedDate}
/>
))}
</ul>
*/
// @flow
import React from 'react';
+import classNames from 'classnames';
import Events from './Events';
import AddEventForm from './forms/AddEventForm';
import RemoveAnalysisForm from './forms/RemoveAnalysisForm';
changeEvent: (event: string, name: string) => Promise<*>,
deleteAnalysis: (analysis: string) => Promise<*>,
deleteEvent: (analysis: string, event: string) => Promise<*>,
- isFirst: boolean
+ isFirst: boolean,
+ selected: boolean,
+ updateSelectedDate: Date => void
};
-export default function ProjectActivityAnalysis(props: Props) {
- const { date, events } = props.analysis;
- const { isFirst, canAdmin } = props;
- const analysisTitle = translate('project_activity.analysis');
- const hasVersion = events.find(event => event.category === 'VERSION') != null;
- return (
- <li className="project-activity-analysis clearfix">
- <div className="project-activity-time spacer-right">
- <FormattedDate date={date} format="LT" tooltipFormat="LTS" />
- </div>
- <div
- className="project-activity-analysis-icon little-spacer-top big-spacer-right"
- title={analysisTitle}
- />
+export default class ProjectActivityAnalysis extends React.PureComponent {
+ props: Props;
- {canAdmin &&
- <div className="project-activity-analysis-actions spacer-left">
- <div className="dropdown display-inline-block">
- <button
- className="js-analysis-actions button-small button-compact dropdown-toggle"
- data-toggle="dropdown">
- <i className="icon-settings" />
- {' '}
- <i className="icon-dropdown" />
- </button>
- <ul className="dropdown-menu dropdown-menu-right">
- {!hasVersion &&
+ handleClick = () => this.props.updateSelectedDate(this.props.analysis.date);
+
+ render() {
+ const { analysis, isFirst, canAdmin } = this.props;
+ const { date, events } = analysis;
+ const analysisTitle = translate('project_activity.analysis');
+ const hasVersion = events.find(event => event.category === 'VERSION') != null;
+ return (
+ <li
+ className={classNames('project-activity-analysis clearfix', {
+ selected: this.props.selected
+ })}
+ data-date={date.valueOf()}
+ onClick={this.handleClick}
+ role="listitem"
+ tabIndex="0">
+ <div className="project-activity-time spacer-right">
+ <FormattedDate date={date} format="LT" tooltipFormat="LTS" />
+ </div>
+ <div
+ className="project-activity-analysis-icon little-spacer-top big-spacer-right"
+ title={analysisTitle}
+ />
+
+ {canAdmin &&
+ <div className="project-activity-analysis-actions spacer-left">
+ <div className="dropdown display-inline-block">
+ <button
+ className="js-analysis-actions button-small button-compact dropdown-toggle"
+ data-toggle="dropdown">
+ <i className="icon-settings" />
+ {' '}
+ <i className="icon-dropdown" />
+ </button>
+ <ul className="dropdown-menu dropdown-menu-right">
+ {!hasVersion &&
+ <li>
+ <AddEventForm
+ addEvent={this.props.addVersion}
+ analysis={analysis}
+ addEventButtonText="project_activity.add_version"
+ />
+ </li>}
<li>
<AddEventForm
- addEvent={props.addVersion}
- analysis={props.analysis}
- addEventButtonText="project_activity.add_version"
- />
- </li>}
- <li>
- <AddEventForm
- addEvent={props.addCustomEvent}
- analysis={props.analysis}
- addEventButtonText="project_activity.add_custom_event"
- />
- </li>
- {!isFirst && <li role="separator" className="divider" />}
- {!isFirst &&
- <li>
- <RemoveAnalysisForm
- analysis={props.analysis}
- deleteAnalysis={props.deleteAnalysis}
+ addEvent={this.props.addCustomEvent}
+ analysis={analysis}
+ addEventButtonText="project_activity.add_custom_event"
/>
- </li>}
- </ul>
- </div>
- </div>}
+ </li>
+ {!isFirst && <li role="separator" className="divider" />}
+ {!isFirst &&
+ <li>
+ <RemoveAnalysisForm
+ analysis={analysis}
+ deleteAnalysis={this.props.deleteAnalysis}
+ />
+ </li>}
+ </ul>
+ </div>
+ </div>}
- {events.length > 0 &&
- <Events
- analysis={props.analysis.key}
- canAdmin={canAdmin}
- changeEvent={props.changeEvent}
- deleteEvent={props.deleteEvent}
- events={events}
- isFirst={props.isFirst}
- />}
+ {events.length > 0 &&
+ <Events
+ analysis={analysis.key}
+ canAdmin={canAdmin}
+ changeEvent={this.props.changeEvent}
+ deleteEvent={this.props.deleteEvent}
+ events={events}
+ isFirst={this.props.isFirst}
+ />}
- </li>
- );
+ </li>
+ );
+ }
}
deleteEvent={this.props.deleteEvent}
loading={this.props.loading}
query={this.props.query}
+ updateQuery={this.props.updateQuery}
/>
</div>
<div className="project-activity-layout-page-main">
};
type State = {
- selectedDate?: ?Date,
graphStartDate: ?Date,
graphEndDate: ?Date,
series: Array<Serie>
}
};
- updateSelectedDate = (selectedDate: ?Date) => this.setState({ selectedDate });
+ updateSelectedDate = (selectedDate: ?Date) => this.props.updateQuery({ selectedDate });
updateGraphZoom = (graphStartDate: ?Date, graphEndDate: ?Date) => {
if (graphEndDate != null && graphStartDate != null) {
measuresHistory={this.props.measuresHistory}
metricsType={metricsType}
project={this.props.project}
- selectedDate={this.state.selectedDate}
+ selectedDate={this.props.query.selectedDate}
series={series}
updateGraphZoom={this.updateGraphZoom}
updateSelectedDate={this.updateSelectedDate}
};
type State = {
+ selectedDate?: ?Date,
tooltipIdx: ?number,
tooltipXPos: ?number
};
hasSeriesData = () => some(this.props.series, serie => serie.data && serie.data.length > 2);
- updateTooltipPos = (tooltipXPos: ?number, tooltipIdx: ?number) =>
- this.setState({ tooltipXPos, tooltipIdx });
+ updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) =>
+ this.setState({ selectedDate, tooltipXPos, tooltipIdx });
render() {
const { loading } = this.props;
</div>
);
}
-
- const { graph, selectedDate, series } = this.props;
+ const { selectedDate, tooltipIdx, tooltipXPos } = this.state;
+ const { graph, series } = this.props;
return (
<div className="project-activity-graph-container">
<StaticGraphsLegend series={series} />
formatYTick={this.formatValue}
leakPeriodDate={this.props.leakPeriodDate}
metricType={this.props.metricsType}
- selectedDate={selectedDate}
+ selectedDate={this.props.selectedDate}
series={series}
showAreas={['coverage', 'duplications'].includes(graph)}
startDate={this.props.graphStartDate}
updateSelectedDate={this.props.updateSelectedDate}
- updateTooltipPos={this.updateTooltipPos}
+ updateTooltip={this.updateTooltip}
updateZoom={this.props.updateGraphZoom}
/>
{selectedDate != null &&
- this.state.tooltipXPos != null &&
+ tooltipXPos != null &&
<GraphsTooltips
formatValue={this.formatValue}
graph={graph}
measuresHistory={this.props.measuresHistory}
selectedDate={selectedDate}
series={series}
- tooltipIdx={this.state.tooltipIdx}
- tooltipPos={this.state.tooltipXPos}
+ tooltipIdx={tooltipIdx}
+ tooltipPos={tooltipXPos}
/>}
</div>
)}
"project": "org.sonarsource.sonarqube:sonarqube",
}
}
+ updateQuery={[Function]}
/>
</div>
<div
cursor: pointer;
}
+.project-activity-analysis.selected {
+ background-color: #ecf6fe;
+}
+
+.project-activity-analysis:focus {
+ outline: none;
+}
+
.project-activity-analysis:hover {
background-color: #ecf6fe;
}
from?: Date,
graph: string,
project: string,
- to?: Date
+ to?: Date,
+ selectedDate?: Date
};
};
export const activityQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
- prevQuery.category !== nextQuery.category ||
- prevQuery.from !== nextQuery.from ||
- prevQuery.to !== nextQuery.to;
+ prevQuery.category !== nextQuery.category || datesQueryChanged(prevQuery, nextQuery);
-export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
- prevQuery.from !== nextQuery.from || prevQuery.to !== nextQuery.to;
+export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => {
+ const nextFrom = nextQuery.from ? nextQuery.from.valueOf() : null;
+ const previousFrom = prevQuery.from ? prevQuery.from.valueOf() : null;
+ const nextTo = nextQuery.to ? nextQuery.to.valueOf() : null;
+ const previousTo = prevQuery.to ? prevQuery.to.valueOf() : null;
+ return previousFrom !== nextFrom || previousTo !== nextTo;
+};
export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
prevQuery.graph !== nextQuery.graph;
+export const selectedDateQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => {
+ const nextSelectedDate = nextQuery.selectedDate ? nextQuery.selectedDate.valueOf() : null;
+ const previousSelectedDate = prevQuery.selectedDate ? prevQuery.selectedDate.valueOf() : null;
+ return nextSelectedDate !== previousSelectedDate;
+};
+
export const generateCoveredLinesMetric = (
uncoveredLines: MeasureHistory,
measuresHistory: Array<MeasureHistory>,
from: parseAsDate(urlQuery['from']),
graph: parseGraph(urlQuery['graph']),
project: parseAsString(urlQuery['id']),
- to: parseAsDate(urlQuery['to'])
+ to: parseAsDate(urlQuery['to']),
+ selectedDate: parseAsDate(urlQuery['selected_date'])
});
export const serializeQuery = (query: Query): RawQuery =>
from: serializeDate(query.from),
graph: serializeGraph(query.graph),
id: serializeString(query.project),
- to: serializeDate(query.to)
+ to: serializeDate(query.to),
+ selected_date: serializeDate(query.selectedDate)
});
};
showEventMarkers?: boolean,
startDate: ?Date,
updateSelectedDate?: (selectedDate: ?Date) => void,
- updateTooltipPos?: (tooltipXPos: ?number, tooltipIdx: ?number) => void,
+ updateTooltip?: (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) => void,
updateZoom?: (start: ?Date, endDate: ?Date) => void,
zoomSpeed: number
};
maxXRange: Array<number>,
mouseOver?: boolean,
mouseOverlayPos?: { [string]: number },
+ selectedDate: ?Date,
selectedDateXPos: ?number,
selectedDateIdx: ?number,
yScale: Scale,
constructor(props: Props) {
super(props);
const scales = this.getScales(props);
- this.state = { ...scales, ...this.getSelectedDatePos(scales.xScale, props.selectedDate) };
- this.updateSelectedDate = throttle(this.updateSelectedDate, 40);
+ const selectedDatePos = this.getSelectedDatePos(scales.xScale, props.selectedDate);
+ this.state = { ...scales, ...selectedDatePos };
+ this.updateTooltipPos = throttle(this.updateTooltipPos, 40);
}
componentWillReceiveProps(nextProps: Props) {
const xScale = scales ? scales.xScale : this.state.xScale;
const selectedDatePos = this.getSelectedDatePos(xScale, nextProps.selectedDate);
this.setState({ ...scales, ...selectedDatePos });
- if (nextProps.updateTooltipPos) {
- nextProps.updateTooltipPos(
+ if (nextProps.updateTooltip) {
+ nextProps.updateTooltip(
+ selectedDatePos.selectedDate,
selectedDatePos.selectedDateXPos,
selectedDatePos.selectedDateIdx
);
this.props.series.some(serie => serie.data[idx].y || serie.data[idx].y === 0)
) {
return {
+ selectedDate,
selectedDateXPos: xScale(selectedDate),
selectedDateIdx: idx
};
}
}
- return { selectedDateXPos: null, selectedDateIdx: null };
+ return { selectedDate: null, selectedDateXPos: null, selectedDateIdx: null };
};
getEventMarker = (size: number) => {
handleMouseMove = (evt: MouseEvent & { target: HTMLElement }) => {
const parentBbox = this.getMouseOverlayPos(evt.target);
- this.updateSelectedDate(evt.pageX - parentBbox.left);
+ this.updateTooltipPos(evt.pageX - parentBbox.left);
};
handleMouseEnter = () => this.setState({ mouseOver: true });
handleMouseOut = (evt: Event & { relatedTarget: HTMLElement }) => {
- const { updateSelectedDate } = this.props;
+ const { updateTooltip } = this.props;
const targetClass = evt.relatedTarget && typeof evt.relatedTarget.className === 'string'
? evt.relatedTarget.className
: '';
if (
- !updateSelectedDate ||
+ !updateTooltip ||
targetClass.includes('bubble-popup') ||
targetClass.includes('graph-tooltip')
) {
return;
}
- this.setState({ mouseOver: false });
- updateSelectedDate(null);
+ this.setState({
+ mouseOver: false,
+ selectedDate: null,
+ selectedDateXPos: null,
+ selectedDateIdx: null
+ });
+ updateTooltip(null, null, null);
};
- updateSelectedDate = (xPos: number) => {
+ handleClick = () => {
const { updateSelectedDate } = this.props;
+ if (updateSelectedDate) {
+ updateSelectedDate(this.state.selectedDate);
+ }
+ };
+
+ updateTooltipPos = (xPos: number) => {
const firstSerie = this.props.series[0];
- if (this.state.mouseOver && firstSerie && updateSelectedDate) {
+ if (this.state.mouseOver && firstSerie) {
+ const { updateTooltip } = this.props;
const date = this.state.xScale.invert(xPos);
const bisectX = bisector(d => d.x).right;
let idx = bisectX(firstSerie.data, date);
if (!nextPoint || (previousPoint && date - previousPoint.x <= nextPoint.x - date)) {
idx--;
}
- updateSelectedDate(firstSerie.data[idx].x);
+ const selectedDate = firstSerie.data[idx].x;
+ const xPos = this.state.xScale(selectedDate);
+ this.setState({ selectedDate, selectedDateXPos: xPos, selectedDateIdx: idx });
+ if (updateTooltip) {
+ updateTooltip(selectedDate, xPos, idx);
+ }
}
}
};
if (zoomEnabled) {
mouseEvents.onWheel = this.handleWheel;
}
- if (this.props.updateSelectedDate) {
+ if (this.props.updateTooltip) {
mouseEvents.onMouseEnter = this.handleMouseEnter;
mouseEvents.onMouseMove = this.handleMouseMove;
mouseEvents.onMouseOut = this.handleMouseOut;
}
+ if (this.props.updateSelectedDate) {
+ mouseEvents.onClick = this.handleClick;
+ }
return (
<rect
className="chart-mouse-events-overlay"