type Props = {
history: History,
- project: string
+ project: string,
+ router: { replace: ({ pathname: string, query?: {} }) => void }
};
type State = {
history={this.props.history}
project={this.props.project}
metrics={this.state.metrics}
+ router={this.props.router}
/>
{this.renderList(analyses)}
// @flow
import React from 'react';
import { map } from 'lodash';
-import { Link } from 'react-router';
import { AutoSizer } from 'react-virtualized';
import { generateSeries, GRAPHS_METRICS_DISPLAYED } from '../../projectActivity/utils';
import { getGraph } from '../../../helpers/storage';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
+import PreviewGraphTooltips from './PreviewGraphTooltips';
+import { formatMeasure, getShortType } from '../../../helpers/measures';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
import type { History, Metric } from '../types';
type Props = {
history: History,
metrics: Array<Metric>,
- project: string
+ project: string,
+ router: { replace: ({ pathname: string, query?: {} }) => void }
};
type State = {
graph: string,
metricsType: string,
- series: Array<Serie>
+ selectedDate: ?Date,
+ series: Array<Serie>,
+ tooltipIdx: ?number,
+ tooltipXPos: ?number
};
+const GRAPH_PADDING = [4, 0, 4, 0];
+
export default class PreviewGraph extends React.PureComponent {
props: Props;
state: State;
this.state = {
graph,
metricsType,
- series: this.getSeries(props.history, graph, metricsType)
+ selectedDate: null,
+ series: this.getSeries(props.history, graph, metricsType),
+ tooltipIdx: null,
+ tooltipXPos: null
};
}
}
}
- getDisplayedMetrics = (graph: string) => {
- const metrics = GRAPHS_METRICS_DISPLAYED[graph];
+ formatValue = (tick: number | string) =>
+ formatMeasure(tick, getShortType(this.state.metricsType));
+
+ getDisplayedMetrics = (graph: string): Array<string> => {
+ const metrics: Array<string> = GRAPHS_METRICS_DISPLAYED[graph];
if (!metrics || metrics.length <= 0) {
return GRAPHS_METRICS_DISPLAYED['overview'];
}
return metrics;
};
- getSeries = (history: History, graph: string, metricsType: string): Array<Serie> => {
+ getSeries = (history: History, graph: string, metricsType: string) => {
const measureHistory = map(history, (item, key) => ({
metric: key,
history: item.filter(p => p.value != null)
return metric ? metric.type : 'INT';
};
+ handleClick = () => {
+ this.props.router.replace({ pathname: '/project/activity', query: { id: this.props.project } });
+ };
+
+ updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) =>
+ this.setState({ selectedDate, tooltipXPos, tooltipIdx });
+
render() {
+ const { graph, selectedDate, tooltipIdx, tooltipXPos } = this.state;
return (
- <div className="big-spacer-bottom spacer-top">
- <Link
- className="overview-analysis-graph"
- to={{ pathname: '/project/activity', query: { id: this.props.project } }}>
- <AutoSizer disableHeight={true}>
- {({ width }) => (
+ <div
+ className="overview-analysis-graph big-spacer-bottom spacer-top"
+ onClick={this.handleClick}
+ tabIndex={0}
+ role="link">
+ <AutoSizer disableHeight={true}>
+ {({ width }) => (
+ <div>
<AdvancedTimeline
endDate={null}
startDate={null}
hideXAxis={true}
interpolate="linear"
metricType={this.state.metricsType}
- padding={[4, 0, 4, 0]}
+ padding={GRAPH_PADDING}
series={this.state.series}
- showAreas={['coverage', 'duplications'].includes(this.state.graph)}
+ showAreas={['coverage', 'duplications'].includes(graph)}
+ updateTooltip={this.updateTooltip}
/>
- )}
- </AutoSizer>
- </Link>
+ {selectedDate != null &&
+ tooltipXPos != null &&
+ tooltipIdx != null &&
+ <PreviewGraphTooltips
+ formatValue={this.formatValue}
+ graph={graph}
+ graphWidth={width}
+ metrics={this.props.metrics}
+ selectedDate={selectedDate}
+ series={this.state.series}
+ tooltipIdx={tooltipIdx}
+ tooltipPos={tooltipXPos}
+ />}
+ </div>
+ )}
+ </AutoSizer>
</div>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import BubblePopup from '../../../components/common/BubblePopup';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent';
+import type { Metric } from '../types';
+import type { Serie } from '../../../components/charts/AdvancedTimeline';
+
+type Props = {
+ formatValue: (number | string) => string,
+ graph: string,
+ graphWidth: number,
+ metrics: Array<Metric>,
+ selectedDate: Date,
+ series: Array<Serie & { translatedName: string }>,
+ tooltipIdx: number,
+ tooltipPos: number
+};
+
+const TOOLTIP_WIDTH = 150;
+
+export default class PreviewGraphTooltips extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { tooltipIdx } = this.props;
+ const top = 16;
+ let left = this.props.tooltipPos;
+ let customClass;
+ if (left > this.props.graphWidth - TOOLTIP_WIDTH + 20) {
+ left -= TOOLTIP_WIDTH;
+ customClass = 'bubble-popup-right';
+ }
+
+ return (
+ <BubblePopup customClass={customClass} position={{ top, left, width: TOOLTIP_WIDTH }}>
+ <div className="overview-analysis-graph-tooltip">
+ <div className="overview-analysis-graph-tooltip-title">
+ <FormattedDate date={this.props.selectedDate} format="LL" />
+ </div>
+ <table className="width-100">
+ <tbody>
+ {this.props.series.map(serie => {
+ const point = serie.data[tooltipIdx];
+ if (!point || (!point.y && point.y !== 0)) {
+ return null;
+ }
+ const metric = this.props.metrics.find(metric => metric.key === serie.name);
+ return (
+ <PreviewGraphTooltipsContent
+ key={serie.name}
+ serie={serie}
+ translatedName={metric && metric.custom ? metric.name : serie.translatedName}
+ value={this.props.formatValue(point.y)}
+ />
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ </BubblePopup>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+import type { Serie } from '../../../components/charts/AdvancedTimeline';
+
+type Props = {
+ serie: Serie,
+ translatedName: string,
+ value: string
+};
+
+export default function PreviewGraphTooltipsContent({ serie, translatedName, value }: Props) {
+ return (
+ <tr className="overview-analysis-graph-tooltip-line">
+ <td className="thin">
+ <ChartLegendIcon
+ className={'little-spacer-right line-chart-legend line-chart-legend-' + serie.style}
+ />
+ </td>
+ <td className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin">
+ {value}
+ </td>
+ <td className="text-ellipsis">{translatedName}</td>
+ </tr>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import PreviewGraphTooltips from '../PreviewGraphTooltips';
+
+const SERIES_OVERVIEW = [
+ {
+ name: 'code_smells',
+ translatedName: 'Code Smells',
+ style: 1,
+ data: [
+ {
+ x: '2011-10-01T22:01:00.000Z',
+ y: 18
+ },
+ {
+ x: '2011-10-25T10:27:41.000Z',
+ y: 15
+ }
+ ]
+ },
+ {
+ name: 'bugs',
+ translatedName: 'Bugs',
+ style: 0,
+ data: [
+ {
+ x: '2011-10-01T22:01:00.000Z',
+ y: 3
+ },
+ {
+ x: '2011-10-25T10:27:41.000Z',
+ y: 0
+ }
+ ]
+ },
+ {
+ name: 'vulnerabilities',
+ translatedName: 'Vulnerabilities',
+ style: 2,
+ data: [
+ {
+ x: '2011-10-01T22:01:00.000Z',
+ y: 0
+ },
+ {
+ x: '2011-10-25T10:27:41.000Z',
+ y: 1
+ }
+ ]
+ }
+];
+
+const METRICS = [
+ { key: 'bugs', name: 'Bugs', type: 'INT' },
+ { key: 'vulnerabilities', name: 'Vulnerabilities', type: 'INT', custom: true }
+];
+
+const DEFAULT_PROPS = {
+ formatValue: val => 'Formated.' + val,
+ graph: 'overview',
+ graphWidth: 150,
+ metrics: METRICS,
+ selectedDate: new Date('2011-10-01T22:01:00.000Z'),
+ series: SERIES_OVERVIEW,
+ tooltipIdx: 0,
+ tooltipPos: 25
+};
+
+it('should render correctly', () => {
+ expect(
+ shallow(
+ <PreviewGraphTooltips
+ {...DEFAULT_PROPS}
+ graph="random"
+ selectedDate={new Date('2011-10-25T10:27:41.000Z')}
+ tooltipIdx={1}
+ />
+ )
+ ).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import PreviewGraphTooltipsContent from '../PreviewGraphTooltipsContent';
+
+const DEFAULT_PROPS = {
+ serie: {
+ name: 'code_smells',
+ translatedName: 'metric.code_smells.name',
+ style: 1
+ },
+ translatedName: 'Code Smells',
+ value: '1.2k'
+};
+
+it('should render correctly', () => {
+ expect(shallow(<PreviewGraphTooltipsContent {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<BubblePopup
+ customClass="bubble-popup-right"
+ position={
+ Object {
+ "left": -125,
+ "top": 16,
+ "width": 150,
+ }
+ }
+>
+ <div
+ className="overview-analysis-graph-tooltip"
+ >
+ <div
+ className="overview-analysis-graph-tooltip-title"
+ >
+ <FormattedDate
+ date={2011-10-25T10:27:41.000Z}
+ format="LL"
+ />
+ </div>
+ <table
+ className="width-100"
+ >
+ <tbody>
+ <PreviewGraphTooltipsContent
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 18,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 15,
+ },
+ ],
+ "name": "code_smells",
+ "style": 1,
+ "translatedName": "Code Smells",
+ }
+ }
+ translatedName="Code Smells"
+ value="Formated.15"
+ />
+ <PreviewGraphTooltipsContent
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 3,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 0,
+ },
+ ],
+ "name": "bugs",
+ "style": 0,
+ "translatedName": "Bugs",
+ }
+ }
+ translatedName="Bugs"
+ value="Formated.0"
+ />
+ <PreviewGraphTooltipsContent
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 0,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 1,
+ },
+ ],
+ "name": "vulnerabilities",
+ "style": 2,
+ "translatedName": "Vulnerabilities",
+ }
+ }
+ translatedName="Vulnerabilities"
+ value="Formated.1"
+ />
+ </tbody>
+ </table>
+ </div>
+</BubblePopup>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tr
+ className="overview-analysis-graph-tooltip-line"
+>
+ <td
+ className="thin"
+ >
+ <ChartLegendIcon
+ className="little-spacer-right line-chart-legend line-chart-legend-1"
+ />
+ </td>
+ <td
+ className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin"
+ >
+ 1.2k
+ </td>
+ <td
+ className="text-ellipsis"
+ >
+ Code Smells
+ </td>
+</tr>
+`;
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
+import { withRouter } from 'react-router';
import { connect } from 'react-redux';
import MetaKey from './MetaKey';
import MetaOrganizationKey from './MetaOrganizationKey';
import MetaTags from './MetaTags';
import { areThereCustomOrganizations } from '../../../store/rootReducer';
-const Meta = ({ component, history, measures, areThereCustomOrganizations }) => {
+const Meta = ({ component, history, measures, areThereCustomOrganizations, router }) => {
const { qualifier, description, qualityProfiles, qualityGate } = component;
const isProject = qualifier === 'TRK';
{shouldShowOrganizationKey && <MetaOrganizationKey component={component} />}
- {isProject && <AnalysesList project={component.key} history={history} />}
+ {isProject && <AnalysesList project={component.key} history={history} router={router} />}
</div>
);
};
areThereCustomOrganizations: areThereCustomOrganizations(state)
});
-export default connect(mapStateToProps)(Meta);
+export default connect(mapStateToProps)(withRouter(Meta));
.overview-analysis-graph {
display: block;
+ cursor: pointer;
outline: none;
border: none;
}
+.overview-analysis-graph .bubble-popup {
+ opacity: 0.8;
+ padding: 0;
+}
+
+.overview-analysis-graph-tooltip {
+ padding: 4px;
+ pointer-events: none;
+ font-size: 12px;
+ overflow: hidden;
+}
+
+.overview-analysis-graph-tooltip-line {
+ padding-bottom: 2px;
+}
+
+.overview-analysis-graph-tooltip-title {
+ font-weight: bold;
+ margin-bottom: 4px;
+}
+
+.overview-analysis-graph-tooltip-value {
+ font-weight: bold;
+}
+
.overview-analysis-event {}
.overview-analysis-event.badge {
metrics: Array<Metric>,
metricsType: string,
removeCustomMetric: (metric: string) => void,
- selectedDate?: ?Date => void,
+ selectedDate: ?Date,
series: Array<Serie>,
updateGraphZoom: (from: ?Date, to: ?Date) => void,
updateSelectedDate: (selectedDate: ?Date) => void
tooltipXPos: null
};
- formatValue = tick => formatMeasure(tick, getShortType(this.props.metricsType));
+ formatValue = (tick: string | number) =>
+ formatMeasure(tick, getShortType(this.props.metricsType));
getEvents = () => {
const { analyses, eventFilter } = this.props;
graph={graph}
graphWidth={width}
measuresHistory={this.props.measuresHistory}
+ metrics={this.props.metrics}
selectedDate={selectedDate}
series={series}
tooltipIdx={tooltipIdx}
import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage';
import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication';
import GraphsTooltipsContentOverview from './GraphsTooltipsContentOverview';
-import type { Event, MeasureHistory } from '../types';
+import type { Event, MeasureHistory, Metric } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
graph: string,
graphWidth: number,
measuresHistory: Array<MeasureHistory>,
+ metrics: Array<Metric>,
selectedDate: Date,
series: Array<Serie & { translatedName: string }>,
tooltipIdx: number,
if (!point || (!point.y && point.y !== 0)) {
return null;
}
- return this.props.graph === 'overview'
- ? <GraphsTooltipsContentOverview
+ if (this.props.graph === 'overview') {
+ return (
+ <GraphsTooltipsContentOverview
key={serie.name}
measuresHistory={measuresHistory}
serie={serie}
tooltipIdx={tooltipIdx}
value={this.props.formatValue(point.y)}
/>
- : <GraphsTooltipsContent
+ );
+ } else {
+ const metric = this.props.metrics.find(metric => metric.key === serie.name);
+ return (
+ <GraphsTooltipsContent
key={serie.name}
serie={serie}
+ translatedName={metric && metric.custom ? metric.name : serie.translatedName}
value={this.props.formatValue(point.y)}
- />;
+ />
+ );
+ }
})}
</tbody>
{this.props.graph === 'coverage' &&
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
- serie: Serie & { translatedName: string },
+ serie: Serie,
+ translatedName: string,
value: string
};
-export default function GraphsTooltipsContent({ serie, value }: Props) {
+export default function GraphsTooltipsContent({ serie, translatedName, value }: Props) {
return (
<tr key={serie.name} className="project-activity-graph-tooltip-line">
<td className="thin">
<td className="project-activity-graph-tooltip-value text-right spacer-right thin">
{value}
</td>
- <td>{serie.translatedName}</td>
+ <td>{translatedName}</td>
</tr>
);
}
const SERIES_OVERVIEW = [
{
name: 'code_smells',
- translatedName: 'Code Smells',
+ translatedName: 'metric.code_smells.name',
style: 1,
data: [
{
},
{
name: 'bugs',
- translatedName: 'Bugs',
+ translatedName: 'metric.bugs.name',
style: 0,
data: [
{
},
{
name: 'vulnerabilities',
- translatedName: 'Vulnerabilities',
+ translatedName: 'metric.vulnerabilities.name',
style: 2,
data: [
{
}
];
+const METRICS = [
+ { key: 'bugs', name: 'Bugs', type: 'INT' },
+ { key: 'vulnerabilities', name: 'Vulnerabilities', type: 'INT', custom: true }
+];
+
const DEFAULT_PROPS = {
formatValue: val => 'Formated.' + val,
graph: 'overview',
graphWidth: 500,
measuresHistory: [],
+ metrics: METRICS,
selectedDate: new Date('2011-10-01T22:01:00.000Z'),
series: SERIES_OVERVIEW,
tooltipIdx: 0,
const DEFAULT_PROPS = {
serie: {
name: 'code_smells',
- translatedName: 'Code Smells',
+ translatedName: 'metric.code_smells.name',
style: 1
},
+ translatedName: 'Code Smells',
value: '1.2k'
};
],
"name": "code_smells",
"style": 1,
- "translatedName": "Code Smells",
+ "translatedName": "metric.code_smells.name",
}
}
tooltipIdx={0}
],
"name": "bugs",
"style": 0,
- "translatedName": "Bugs",
+ "translatedName": "metric.bugs.name",
}
}
tooltipIdx={0}
],
"name": "vulnerabilities",
"style": 2,
- "translatedName": "Vulnerabilities",
+ "translatedName": "metric.vulnerabilities.name",
}
}
tooltipIdx={0}
],
"name": "code_smells",
"style": 1,
- "translatedName": "Code Smells",
+ "translatedName": "metric.code_smells.name",
}
}
+ translatedName="metric.code_smells.name"
value="Formated.15"
/>
<GraphsTooltipsContent
],
"name": "bugs",
"style": 0,
- "translatedName": "Bugs",
+ "translatedName": "metric.bugs.name",
}
}
+ translatedName="metric.bugs.name"
value="Formated.0"
/>
<GraphsTooltipsContent
],
"name": "vulnerabilities",
"style": 2,
- "translatedName": "Vulnerabilities",
+ "translatedName": "metric.vulnerabilities.name",
}
}
+ translatedName="Vulnerabilities"
value="Formated.1"
/>
</tbody>
.little-spacer-bottom { margin-bottom: 4px; }
.little-spacer-top { margin-top: 4px; }
+td.little-spacer-left { padding-left: 4px; }
+td.little-spacer-right { padding-right: 4px; }
+
td.spacer-left { padding-left: 8px; }
td.spacer-right { padding-right: 8px; }
td.spacer-bottom { padding-bottom: 8px; }