import { AutoSizer } from 'react-virtualized';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
import GraphsTooltips from './GraphsTooltips';
+import GraphsLegendCustom from './GraphsLegendCustom';
import GraphsLegendStatic from './GraphsLegendStatic';
import { formatMeasure, getShortType } from '../../../helpers/measures';
import { EVENT_TYPES, hasHistoryData, isCustomGraph } from '../utils';
import { translate } from '../../../helpers/l10n';
-import type { Analysis, MeasureHistory } from '../types';
+import type { Analysis, MeasureHistory, Metric } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
leakPeriodDate: Date,
loading: boolean,
measuresHistory: Array<MeasureHistory>,
+ metrics: Array<Metric>,
metricsType: string,
+ removeCustomMetric: (metric: string) => void,
selectedDate?: ?Date => void,
series: Array<Serie>,
updateGraphZoom: (from: ?Date, to: ?Date) => void,
render() {
const { loading } = this.props;
const { graph, series } = this.props;
+ const isCustom = isCustomGraph(graph);
if (loading) {
return (
<div className="project-activity-graph-container">
<div className="note text-center">
{translate(
- isCustomGraph(this.props.graph)
+ isCustom
? 'project_activity.graphs.custom.no_history'
: 'component_measures.no_history'
)}
const { selectedDate, tooltipIdx, tooltipXPos } = this.state;
return (
<div className="project-activity-graph-container">
- <GraphsLegendStatic series={series} />
+ {isCustom
+ ? <GraphsLegendCustom
+ series={series}
+ metrics={this.props.metrics}
+ removeMetric={this.props.removeCustomMetric}
+ />
+ : <GraphsLegendStatic series={series} />}
<div className="project-activity-graph">
<AutoSizer>
{({ height, width }) => (
--- /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 GraphsLegendItem from './GraphsLegendItem';
+import type { Metric } from '../types';
+
+type Props = {
+ metrics: Array<Metric>,
+ removeMetric: string => void,
+ series: Array<{ name: string, translatedName: string, style: string }>
+};
+
+export default function GraphsLegendCustom({ metrics, removeMetric, series }: Props) {
+ return (
+ <div className="project-activity-graph-legends">
+ {series.map(serie => {
+ const metric = metrics.find(metric => metric.key === serie.name);
+ return (
+ <span className="spacer-left spacer-right" key={serie.name}>
+ <GraphsLegendItem
+ metric={serie.name}
+ name={metric && metric.custom ? metric.name : serie.translatedName}
+ style={serie.style}
+ removeMetric={removeMetric}
+ />
+ </span>
+ );
+ })}
+ </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 classNames from 'classnames';
+import CloseIcon from '../../../components/icons-components/CloseIcon';
+import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+
+type Props = {
+ className?: string,
+ metric: string,
+ name: string,
+ style: string,
+ removeMetric?: string => void
+};
+
+export default class GraphsLegendItem extends React.PureComponent {
+ props: Props;
+
+ handleClick = (e: Event) => {
+ e.preventDefault();
+ this.props.removeMetric(this.props.metric);
+ };
+
+ render() {
+ const isActionable = this.props.removeMetric != null;
+ const legendClass = classNames(
+ {
+ 'project-activity-graph-legend-actionable': isActionable
+ },
+ this.props.className
+ );
+
+ return (
+ <span className={legendClass}>
+ <ChartLegendIcon
+ className={classNames(
+ 'spacer-right line-chart-legend',
+ 'line-chart-legend-' + this.props.style
+ )}
+ />
+ {this.props.name}
+ {isActionable &&
+ <a className="spacer-left button-clean text-text-top" href="#" onClick={this.handleClick}>
+ <CloseIcon className="text-danger" />
+ </a>}
+ </span>
+ );
+ }
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import classNames from 'classnames';
-import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+import GraphsLegendItem from './GraphsLegendItem';
type Props = {
series: Array<{ name: string, translatedName: string, style: string }>
return (
<div className="project-activity-graph-legends">
{series.map(serie => (
- <span className="big-spacer-left big-spacer-right" key={serie.name}>
- <ChartLegendIcon
- className={classNames(
- 'spacer-right line-chart-legend',
- 'line-chart-legend-' + serie.style
- )}
- />
- {serie.translatedName}
- </span>
+ <GraphsLegendItem
+ className="big-spacer-left big-spacer-right"
+ key={serie.name}
+ metric={serie.name}
+ name={serie.translatedName}
+ style={serie.style}
+ />
))}
</div>
);
measuresHistory={measuresHistory}
metrics={this.props.metrics}
metricsType={this.getMetricType()}
- project={this.props.project.key}
query={query}
updateQuery={this.props.updateQuery}
/>
measuresHistory: Array<MeasureHistory>,
metrics: Array<Metric>,
metricsType: string,
- project: string,
query: Query,
updateQuery: RawQuery => void
};
}
};
- updateSelectedDate = (selectedDate: ?Date) => this.props.updateQuery({ selectedDate });
+ addCustomMetric = (metric: string) =>
+ this.props.updateQuery({ customMetrics: [...this.props.query.customMetrics, metric] });
+
+ removeCustomMetric = (removedMetric: string) =>
+ this.props.updateQuery({
+ customMetrics: this.props.query.customMetrics.filter(metric => metric !== removedMetric)
+ });
+
+ updateGraph = (graph: string) => this.props.updateQuery({ graph });
updateGraphZoom = (graphStartDate: ?Date, graphEndDate: ?Date) => {
if (graphEndDate != null && graphStartDate != null) {
this.updateQueryDateRange([graphStartDate, graphEndDate]);
};
+ updateSelectedDate = (selectedDate: ?Date) => this.props.updateQuery({ selectedDate });
+
updateQueryDateRange = (dates: Array<?Date>) => {
if (dates[0] == null || dates[1] == null) {
this.props.updateQuery({ from: dates[0], to: dates[1] });
};
render() {
- const { leakPeriodDate, loading, metricsType, query } = this.props;
+ const { leakPeriodDate, loading, metrics, metricsType, query } = this.props;
const { series } = this.state;
return (
<div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
<ProjectActivityGraphsHeader
+ addCustomMetric={this.addCustomMetric}
graph={query.graph}
- metrics={this.props.metrics}
+ metrics={metrics}
selectedMetrics={this.props.query.customMetrics}
- updateQuery={this.props.updateQuery}
+ updateGraph={this.updateGraph}
/>
<GraphsHistory
analyses={this.props.analyses}
leakPeriodDate={leakPeriodDate}
loading={loading}
measuresHistory={this.props.measuresHistory}
+ metrics={metrics}
metricsType={metricsType}
- project={this.props.project}
+ removeCustomMetric={this.removeCustomMetric}
selectedDate={this.props.query.selectedDate}
series={series}
updateGraphZoom={this.updateGraphZoom}
import { isCustomGraph, GRAPH_TYPES } from '../utils';
import { translate } from '../../../helpers/l10n';
import type { Metric } from '../types';
-import type { RawQuery } from '../../../helpers/query';
type Props = {
+ addCustomMetric: string => void,
graph: string,
metrics: Array<Metric>,
selectedMetrics: Array<string>,
- updateQuery: RawQuery => void
+ updateGraph: string => void
};
export default class ProjectActivityGraphsHeader extends React.PureComponent {
handleGraphChange = (option: { value: string }) => {
if (option.value !== this.props.graph) {
- this.props.updateQuery({ graph: option.value });
+ this.props.updateGraph(option.value);
}
};
- handleAddMetric = (metric: string) => {
- const selectedMetrics = [...this.props.selectedMetrics, metric];
- this.props.updateQuery({ customMetrics: selectedMetrics });
- };
-
render() {
const selectOptions = GRAPH_TYPES.map(graph => ({
label: translate('project_activity.graphs', graph),
/>
{isCustomGraph(this.props.graph) &&
<AddGraphMetric
- addMetric={this.handleAddMetric}
+ addMetric={this.props.addCustomMetric}
className="spacer-left"
metrics={this.props.metrics}
selectedMetrics={this.props.selectedMetrics}
leakPeriodDate: '2017-05-16T13:50:02+0200',
loading: false,
measuresHistory: [],
+ metrics: [],
metricsType: 'INT',
+ removeCustomMetric: () => {},
selectedDate: null,
series: SERIES,
updateGraphZoom: () => {},
--- /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 GraphsLegendCustom from '../GraphsLegendCustom';
+
+const SERIES = [
+ { name: 'bugs', translatedName: 'Bugs', style: '2', data: [] },
+ { name: 'my_metric', translatedName: 'metric.my_metric.name', style: '1', data: [] },
+ { name: 'foo', translatedName: 'Foo', style: '0', data: [] }
+];
+
+const METRICS = [
+ { key: 'bugs', name: 'Bugs' },
+ { key: 'my_metric', name: 'My Metric', custom: true }
+];
+
+it('should render correctly the list of series', () => {
+ expect(
+ shallow(<GraphsLegendCustom metrics={METRICS} removeMetric={() => {}} series={SERIES} />)
+ ).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 GraphsLegendItem from '../GraphsLegendItem';
+
+it('should render correctly a legend', () => {
+ expect(shallow(<GraphsLegendItem metric="bugs" name="Bugs" style="2" />)).toMatchSnapshot();
+});
+
+it('should render correctly an actionable legend', () => {
+ expect(
+ shallow(
+ <GraphsLegendItem
+ className="myclass"
+ metric="foo"
+ name="Foo"
+ style="1"
+ removeMetric={() => {}}
+ />
+ )
+ ).toMatchSnapshot();
+});
}
],
metricsType: 'INT',
- project: 'org.sonarsource.sonarqube:sonarqube',
query: { category: '', graph: 'overview', project: 'org.sonarsource.sonarqube:sonarqube' },
updateQuery: () => {}
};
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the list of series 1`] = `
+<div
+ className="project-activity-graph-legends"
+>
+ <span
+ className="spacer-left spacer-right"
+ >
+ <GraphsLegendItem
+ metric="bugs"
+ name="Bugs"
+ removeMetric={[Function]}
+ style="2"
+ />
+ </span>
+ <span
+ className="spacer-left spacer-right"
+ >
+ <GraphsLegendItem
+ metric="my_metric"
+ name="My Metric"
+ removeMetric={[Function]}
+ style="1"
+ />
+ </span>
+ <span
+ className="spacer-left spacer-right"
+ >
+ <GraphsLegendItem
+ metric="foo"
+ name="Foo"
+ removeMetric={[Function]}
+ style="0"
+ />
+ </span>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly a legend 1`] = `
+<span
+ className=""
+>
+ <ChartLegendIcon
+ className="spacer-right line-chart-legend line-chart-legend-2"
+ />
+ Bugs
+</span>
+`;
+
+exports[`should render correctly an actionable legend 1`] = `
+<span
+ className="project-activity-graph-legend-actionable myclass"
+>
+ <ChartLegendIcon
+ className="spacer-right line-chart-legend line-chart-legend-1"
+ />
+ Foo
+ <a
+ className="spacer-left button-clean text-text-top"
+ href="#"
+ onClick={[Function]}
+ >
+ <CloseIcon
+ className="text-danger"
+ />
+ </a>
+</span>
+`;
<div
className="project-activity-graph-legends"
>
- <span
+ <GraphsLegendItem
className="big-spacer-left big-spacer-right"
- >
- <ChartLegendIcon
- className="spacer-right line-chart-legend line-chart-legend-2"
- />
- Bugs
- </span>
- <span
+ metric="bugs"
+ name="Bugs"
+ style="2"
+ />
+ <GraphsLegendItem
className="big-spacer-left big-spacer-right"
- >
- <ChartLegendIcon
- className="spacer-right line-chart-legend line-chart-legend-1"
- />
- Code Smells
- </span>
+ metric="code_smells"
+ name="Code Smells"
+ style="1"
+ />
</div>
`;
]
}
metricsType="INT"
- project="org.sonarsource.sonarqube:sonarqube"
query={
Object {
"category": "",
className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"
>
<ProjectActivityGraphsHeader
+ addCustomMetric={[Function]}
graph="overview"
- updateQuery={[Function]}
+ updateGraph={[Function]}
/>
<GraphsHistory
analyses={
]
}
metricsType="INT"
- project="org.sonarsource.sonarqube:sonarqube"
+ removeCustomMetric={[Function]}
series={
Array [
Object {
render() {
return (
- <button className={this.props.className} onClick={this.openForm}>
+ <button
+ className={this.props.className}
+ onClick={this.openForm}
+ disabled={this.props.selectedMetrics.length >= 3}>
{translate('project_activity.graphs.custom.add')}
{this.state.open && this.renderModal()}
</button>
text-align: center;
}
+.project-activity-graph-legend-actionable {
+ padding: 4px 12px;
+ border: 1px solid #e6e6e6;
+ border-radius: 12px;
+}
+
.project-activity-graph-tooltip {
padding: 8px;
pointer-events: none;