</div>
<footer className="modal-foot">
<div>
- <button type="submit">{translate('organization.members.add_to_members')}</button>
+ <button type="submit" disabled={!this.state.selectedMember}>
+ {translate('organization.members.add_to_members')}
+ </button>
<button type="reset" className="button-link" onClick={this.closeForm}>
{translate('cancel')}
</button>
>
<div>
<button
+ disabled={true}
type="submit"
>
organization.members.add_to_members
--- /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 moment from 'moment';
+import { sortBy } from 'lodash';
+import { AutoSizer } from 'react-virtualized';
+import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
+import GraphsTooltips from './GraphsTooltips';
+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 { Serie } from '../../../components/charts/AdvancedTimeline';
+
+type Props = {
+ analyses: Array<Analysis>,
+ eventFilter: string,
+ graph: string,
+ graphEndDate: ?Date,
+ graphStartDate: ?Date,
+ leakPeriodDate: Date,
+ loading: boolean,
+ measuresHistory: Array<MeasureHistory>,
+ metricsType: string,
+ selectedDate?: ?Date => void,
+ series: Array<Serie>,
+ updateGraphZoom: (from: ?Date, to: ?Date) => void,
+ updateSelectedDate: (selectedDate: ?Date) => void
+};
+
+type State = {
+ selectedDate?: ?Date,
+ tooltipIdx: ?number,
+ tooltipXPos: ?number
+};
+
+export default class GraphsHistory extends React.PureComponent {
+ props: Props;
+ state: State = {
+ tooltipIdx: null,
+ tooltipXPos: null
+ };
+
+ formatValue = tick => formatMeasure(tick, getShortType(this.props.metricsType));
+
+ getEvents = () => {
+ const { analyses, eventFilter } = this.props;
+ const filteredEvents = analyses.reduce((acc, analysis) => {
+ if (analysis.events.length <= 0) {
+ return acc;
+ }
+ let event;
+ if (eventFilter) {
+ event = analysis.events.filter(event => event.category === eventFilter)[0];
+ } else {
+ event = sortBy(analysis.events, event => EVENT_TYPES.indexOf(event.category))[0];
+ }
+ if (!event) {
+ return acc;
+ }
+ return acc.concat({
+ className: event.category,
+ name: event.name,
+ date: moment(analysis.date).toDate()
+ });
+ }, []);
+ return sortBy(filteredEvents, 'date');
+ };
+
+ getSelectedDateEvents = () => {
+ const { selectedDate } = this.state;
+ const { analyses } = this.props;
+ if (analyses && selectedDate) {
+ const analysis = analyses.find(
+ analysis => analysis.date.valueOf() === selectedDate.valueOf()
+ );
+ if (analysis) {
+ return analysis.events;
+ }
+ }
+ return [];
+ };
+
+ updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) =>
+ this.setState({ selectedDate, tooltipXPos, tooltipIdx });
+
+ render() {
+ const { loading } = this.props;
+ const { graph, series } = this.props;
+
+ if (loading) {
+ return (
+ <div className="project-activity-graph-container">
+ <div className="text-center">
+ <i className="spinner" />
+ </div>
+ </div>
+ );
+ }
+
+ if (!hasHistoryData(series)) {
+ return (
+ <div className="project-activity-graph-container">
+ <div className="note text-center">
+ {translate(
+ isCustomGraph(this.props.graph)
+ ? 'project_activity.graphs.custom.no_history'
+ : 'component_measures.no_history'
+ )}
+ </div>
+ </div>
+ );
+ }
+
+ const { selectedDate, tooltipIdx, tooltipXPos } = this.state;
+ return (
+ <div className="project-activity-graph-container">
+ <GraphsLegendStatic series={series} />
+ <div className="project-activity-graph">
+ <AutoSizer>
+ {({ height, width }) => (
+ <div>
+ <AdvancedTimeline
+ endDate={this.props.graphEndDate}
+ height={height}
+ width={width}
+ interpolate="linear"
+ formatYTick={this.formatValue}
+ leakPeriodDate={this.props.leakPeriodDate}
+ metricType={this.props.metricsType}
+ selectedDate={this.props.selectedDate}
+ series={series}
+ showAreas={['coverage', 'duplications'].includes(graph)}
+ startDate={this.props.graphStartDate}
+ updateSelectedDate={this.props.updateSelectedDate}
+ updateTooltip={this.updateTooltip}
+ updateZoom={this.props.updateGraphZoom}
+ />
+ {selectedDate != null &&
+ tooltipXPos != null &&
+ <GraphsTooltips
+ events={this.getSelectedDateEvents()}
+ formatValue={this.formatValue}
+ graph={graph}
+ graphWidth={width}
+ measuresHistory={this.props.measuresHistory}
+ selectedDate={selectedDate}
+ series={series}
+ tooltipIdx={tooltipIdx}
+ tooltipPos={tooltipXPos}
+ />}
+ </div>
+ )}
+ </AutoSizer>
+ </div>
+ </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 ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+
+type Props = {
+ series: Array<{ name: string, translatedName: string, style: string }>
+};
+
+export default function GraphsLegendStatic({ series }: Props) {
+ 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>
+ ))}
+ </div>
+ );
+}
*/
// @flow
import React from 'react';
-import { some } from 'lodash';
import { AutoSizer } from 'react-virtualized';
import ZoomTimeLine from '../../../components/charts/ZoomTimeLine';
+import { hasHistoryData } from '../utils';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
updateGraphZoom: (from: ?Date, to: ?Date) => void
};
-export default class GraphsZoom extends React.PureComponent {
- props: Props;
-
- hasHistoryData = () => some(this.props.series, serie => serie.data && serie.data.length > 2);
-
- render() {
- const { loading } = this.props;
- if (loading || !this.hasHistoryData()) {
- return null;
- }
-
- return (
- <div className="project-activity-graph-zoom">
- <AutoSizer disableHeight={true}>
- {({ width }) => (
- <ZoomTimeLine
- endDate={this.props.graphEndDate}
- height={64}
- width={width}
- interpolate="linear"
- leakPeriodDate={this.props.leakPeriodDate}
- metricType={this.props.metricsType}
- padding={[0, 10, 18, 60]}
- series={this.props.series}
- showAreas={this.props.showAreas}
- startDate={this.props.graphStartDate}
- updateZoom={this.props.updateGraphZoom}
- />
- )}
- </AutoSizer>
- </div>
- );
+export default function GraphsZoom(props: Props) {
+ const { loading } = props;
+ if (loading || !hasHistoryData(props.series)) {
+ return null;
}
+
+ return (
+ <div className="project-activity-graph-zoom">
+ <AutoSizer disableHeight={true}>
+ {({ width }) => (
+ <ZoomTimeLine
+ endDate={props.graphEndDate}
+ height={64}
+ width={width}
+ interpolate="linear"
+ leakPeriodDate={props.leakPeriodDate}
+ metricType={props.metricsType}
+ padding={[0, 10, 18, 60]}
+ series={props.series}
+ showAreas={props.showAreas}
+ startDate={props.graphStartDate}
+ updateZoom={props.updateGraphZoom}
+ />
+ )}
+ </AutoSizer>
+ </div>
+ );
}
leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()}
loading={this.props.graphLoading}
measuresHistory={measuresHistory}
+ metrics={this.props.metrics}
metricsType={this.getMetricType()}
project={this.props.project.key}
query={query}
import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash';
import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
import GraphsZoom from './GraphsZoom';
-import StaticGraphs from './StaticGraphs';
+import GraphsHistory from './GraphsHistory';
import {
datesQueryChanged,
generateSeries,
historyQueryChanged
} from '../utils';
import type { RawQuery } from '../../../helpers/query';
-import type { Analysis, MeasureHistory, Query } from '../types';
+import type { Analysis, MeasureHistory, Metric, Query } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
leakPeriodDate: Date,
loading: boolean,
measuresHistory: Array<MeasureHistory>,
+ metrics: Array<Metric>,
metricsType: string,
project: string,
query: Query,
const { series } = this.state;
return (
<div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
- <ProjectActivityGraphsHeader graph={query.graph} updateQuery={this.props.updateQuery} />
- <StaticGraphs
+ <ProjectActivityGraphsHeader
+ graph={query.graph}
+ metrics={this.props.metrics}
+ selectedMetrics={this.props.query.customMetrics}
+ updateQuery={this.props.updateQuery}
+ />
+ <GraphsHistory
analyses={this.props.analyses}
eventFilter={query.category}
graph={query.graph}
// @flow
import React from 'react';
import Select from 'react-select';
-import { GRAPH_TYPES } from '../utils';
+import AddGraphMetric from './forms/AddGraphMetric';
+import { isCustomGraph, GRAPH_TYPES } from '../utils';
import { translate } from '../../../helpers/l10n';
+import type { Metric } from '../types';
import type { RawQuery } from '../../../helpers/query';
type Props = {
- updateQuery: RawQuery => void,
- graph: string
+ graph: string,
+ metrics: Array<Metric>,
+ selectedMetrics: Array<string>,
+ updateQuery: RawQuery => void
};
export default class ProjectActivityGraphsHeader extends React.PureComponent {
}
};
+ 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),
options={selectOptions}
onChange={this.handleGraphChange}
/>
+ {isCustomGraph(this.props.graph) &&
+ <AddGraphMetric
+ addMetric={this.handleAddMetric}
+ className="spacer-left"
+ metrics={this.props.metrics}
+ selectedMetrics={this.props.selectedMetrics}
+ />}
</header>
);
}
+++ /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 moment from 'moment';
-import { some, sortBy } from 'lodash';
-import { AutoSizer } from 'react-virtualized';
-import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
-import GraphsTooltips from './GraphsTooltips';
-import StaticGraphsLegend from './StaticGraphsLegend';
-import { formatMeasure, getShortType } from '../../../helpers/measures';
-import { EVENT_TYPES, isCustomGraph } from '../utils';
-import { translate } from '../../../helpers/l10n';
-import type { Analysis, MeasureHistory } from '../types';
-import type { Serie } from '../../../components/charts/AdvancedTimeline';
-
-type Props = {
- analyses: Array<Analysis>,
- eventFilter: string,
- graph: string,
- graphEndDate: ?Date,
- graphStartDate: ?Date,
- leakPeriodDate: Date,
- loading: boolean,
- measuresHistory: Array<MeasureHistory>,
- metricsType: string,
- selectedDate?: ?Date => void,
- series: Array<Serie>,
- updateGraphZoom: (from: ?Date, to: ?Date) => void,
- updateSelectedDate: (selectedDate: ?Date) => void
-};
-
-type State = {
- selectedDate?: ?Date,
- tooltipIdx: ?number,
- tooltipXPos: ?number
-};
-
-export default class StaticGraphs extends React.PureComponent {
- props: Props;
- state: State = {
- tooltipIdx: null,
- tooltipXPos: null
- };
-
- formatValue = tick => formatMeasure(tick, getShortType(this.props.metricsType));
-
- getEvents = () => {
- const { analyses, eventFilter } = this.props;
- const filteredEvents = analyses.reduce((acc, analysis) => {
- if (analysis.events.length <= 0) {
- return acc;
- }
- let event;
- if (eventFilter) {
- event = analysis.events.filter(event => event.category === eventFilter)[0];
- } else {
- event = sortBy(analysis.events, event => EVENT_TYPES.indexOf(event.category))[0];
- }
- if (!event) {
- return acc;
- }
- return acc.concat({
- className: event.category,
- name: event.name,
- date: moment(analysis.date).toDate()
- });
- }, []);
- return sortBy(filteredEvents, 'date');
- };
-
- getSelectedDateEvents = () => {
- const { selectedDate } = this.state;
- const { analyses } = this.props;
- if (analyses && selectedDate) {
- const analysis = analyses.find(
- analysis => analysis.date.valueOf() === selectedDate.valueOf()
- );
- if (analysis) {
- return analysis.events;
- }
- }
- return [];
- };
-
- hasSeriesData = () => some(this.props.series, serie => serie.data && serie.data.length > 2);
-
- updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) =>
- this.setState({ selectedDate, tooltipXPos, tooltipIdx });
-
- render() {
- const { loading } = this.props;
-
- if (loading) {
- return (
- <div className="project-activity-graph-container">
- <div className="text-center">
- <i className="spinner" />
- </div>
- </div>
- );
- }
-
- if (!this.hasSeriesData()) {
- return (
- <div className="project-activity-graph-container">
- <div className="note text-center">
- {translate(
- isCustomGraph(this.props.graph)
- ? 'project_activity.graphs.custom.no_history'
- : 'component_measures.no_history'
- )}
- </div>
- </div>
- );
- }
-
- const { selectedDate, tooltipIdx, tooltipXPos } = this.state;
- const { graph, series } = this.props;
- return (
- <div className="project-activity-graph-container">
- <StaticGraphsLegend series={series} />
- <div className="project-activity-graph">
- <AutoSizer>
- {({ height, width }) => (
- <div>
- <AdvancedTimeline
- endDate={this.props.graphEndDate}
- height={height}
- width={width}
- interpolate="linear"
- formatYTick={this.formatValue}
- leakPeriodDate={this.props.leakPeriodDate}
- metricType={this.props.metricsType}
- selectedDate={this.props.selectedDate}
- series={series}
- showAreas={['coverage', 'duplications'].includes(graph)}
- startDate={this.props.graphStartDate}
- updateSelectedDate={this.props.updateSelectedDate}
- updateTooltip={this.updateTooltip}
- updateZoom={this.props.updateGraphZoom}
- />
- {selectedDate != null &&
- tooltipXPos != null &&
- <GraphsTooltips
- events={this.getSelectedDateEvents()}
- formatValue={this.formatValue}
- graph={graph}
- graphWidth={width}
- measuresHistory={this.props.measuresHistory}
- selectedDate={selectedDate}
- series={series}
- tooltipIdx={tooltipIdx}
- tooltipPos={tooltipXPos}
- />}
- </div>
- )}
- </AutoSizer>
- </div>
- </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 ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
-
-type Props = {
- series: Array<{ name: string, translatedName: string, style: string }>
-};
-
-export default function StaticGraphsLegend({ series }: Props) {
- 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>
- ))}
- </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 { shallow } from 'enzyme';
+import GraphsHistory from '../GraphsHistory';
+
+const ANALYSES = [
+ {
+ key: 'A1',
+ date: new Date('2016-10-27T16:33:50+0200'),
+ events: [
+ {
+ key: 'E1',
+ category: 'VERSION',
+ name: '6.5-SNAPSHOT'
+ }
+ ]
+ },
+ {
+ key: 'A2',
+ date: new Date('2016-10-27T12:21:15+0200'),
+ events: []
+ },
+ {
+ key: 'A3',
+ date: new Date('2016-10-26T12:17:29+0200'),
+ events: [
+ {
+ key: 'E2',
+ category: 'OTHER',
+ name: 'foo'
+ },
+ {
+ key: 'E3',
+ category: 'VERSION',
+ name: '6.4'
+ }
+ ]
+ }
+];
+
+const SERIES = [
+ {
+ name: 'bugs',
+ translatedName: 'metric.bugs.name',
+ style: 0,
+ data: [
+ { x: new Date('2016-10-27T16:33:50+0200'), y: 5 },
+ { x: new Date('2016-10-27T12:21:15+0200'), y: 16 },
+ { x: new Date('2016-10-26T12:17:29+0200'), y: 12 }
+ ]
+ }
+];
+
+const EMPTY_SERIES = [
+ {
+ name: 'bugs',
+ translatedName: 'metric.bugs.name',
+ style: 0,
+ data: []
+ }
+];
+
+const DEFAULT_PROPS = {
+ analyses: ANALYSES,
+ eventFilter: '',
+ graph: 'overview',
+ graphEndDate: null,
+ graphStartDate: null,
+ leakPeriodDate: '2017-05-16T13:50:02+0200',
+ loading: false,
+ measuresHistory: [],
+ metricsType: 'INT',
+ selectedDate: null,
+ series: SERIES,
+ updateGraphZoom: () => {},
+ updateSelectedDate: () => {}
+};
+
+it('should show a loading view', () => {
+ expect(shallow(<GraphsHistory {...DEFAULT_PROPS} loading={true} />)).toMatchSnapshot();
+});
+
+it('should show that there is no data', () => {
+ expect(shallow(<GraphsHistory {...DEFAULT_PROPS} series={EMPTY_SERIES} />)).toMatchSnapshot();
+});
+
+it('should correctly render a graph', () => {
+ expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should correctly filter events', () => {
+ expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />).instance().getEvents()).toMatchSnapshot();
+ expect(
+ shallow(<GraphsHistory {...DEFAULT_PROPS} eventFilter="OTHER" />).instance().getEvents()
+ ).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 GraphsLegendStatic from '../GraphsLegendStatic';
+
+const SERIES = [
+ { name: 'bugs', translatedName: 'Bugs', style: '2', data: [] },
+ { name: 'code_smells', translatedName: 'Code Smells', style: '1', data: [] }
+];
+
+it('should render correctly the list of series', () => {
+ expect(shallow(<GraphsLegendStatic 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 StaticGraphs from '../StaticGraphs';
-
-const ANALYSES = [
- {
- key: 'A1',
- date: new Date('2016-10-27T16:33:50+0200'),
- events: [
- {
- key: 'E1',
- category: 'VERSION',
- name: '6.5-SNAPSHOT'
- }
- ]
- },
- {
- key: 'A2',
- date: new Date('2016-10-27T12:21:15+0200'),
- events: []
- },
- {
- key: 'A3',
- date: new Date('2016-10-26T12:17:29+0200'),
- events: [
- {
- key: 'E2',
- category: 'OTHER',
- name: 'foo'
- },
- {
- key: 'E3',
- category: 'VERSION',
- name: '6.4'
- }
- ]
- }
-];
-
-const SERIES = [
- {
- name: 'bugs',
- translatedName: 'metric.bugs.name',
- style: 0,
- data: [
- { x: new Date('2016-10-27T16:33:50+0200'), y: 5 },
- { x: new Date('2016-10-27T12:21:15+0200'), y: 16 },
- { x: new Date('2016-10-26T12:17:29+0200'), y: 12 }
- ]
- }
-];
-
-const EMPTY_SERIES = [
- {
- name: 'bugs',
- translatedName: 'metric.bugs.name',
- style: 0,
- data: []
- }
-];
-
-const DEFAULT_PROPS = {
- analyses: ANALYSES,
- eventFilter: '',
- graph: 'overview',
- graphEndDate: null,
- graphStartDate: null,
- leakPeriodDate: '2017-05-16T13:50:02+0200',
- loading: false,
- measuresHistory: [],
- metricsType: 'INT',
- selectedDate: null,
- series: SERIES,
- updateGraphZoom: () => {},
- updateSelectedDate: () => {}
-};
-
-it('should show a loading view', () => {
- expect(shallow(<StaticGraphs {...DEFAULT_PROPS} loading={true} />)).toMatchSnapshot();
-});
-
-it('should show that there is no data', () => {
- expect(shallow(<StaticGraphs {...DEFAULT_PROPS} series={EMPTY_SERIES} />)).toMatchSnapshot();
-});
-
-it('should correctly render a graph', () => {
- expect(shallow(<StaticGraphs {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should correctly filter events', () => {
- expect(shallow(<StaticGraphs {...DEFAULT_PROPS} />).instance().getEvents()).toMatchSnapshot();
- expect(
- shallow(<StaticGraphs {...DEFAULT_PROPS} eventFilter="OTHER" />).instance().getEvents()
- ).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 StaticGraphsLegend from '../StaticGraphsLegend';
-
-const SERIES = [
- { name: 'bugs', translatedName: 'Bugs', style: '2', data: [] },
- { name: 'code_smells', translatedName: 'Code Smells', style: '1', data: [] }
-];
-
-it('should render correctly the list of series', () => {
- expect(shallow(<StaticGraphsLegend series={SERIES} />)).toMatchSnapshot();
-});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly filter events 1`] = `
+Array [
+ Object {
+ "className": "VERSION",
+ "date": 2016-10-26T10:17:29.000Z,
+ "name": "6.4",
+ },
+ Object {
+ "className": "VERSION",
+ "date": 2016-10-27T14:33:50.000Z,
+ "name": "6.5-SNAPSHOT",
+ },
+]
+`;
+
+exports[`should correctly filter events 2`] = `
+Array [
+ Object {
+ "className": "OTHER",
+ "date": 2016-10-26T10:17:29.000Z,
+ "name": "foo",
+ },
+]
+`;
+
+exports[`should correctly render a graph 1`] = `
+<div
+ className="project-activity-graph-container"
+>
+ <GraphsLegendStatic
+ series={
+ Array [
+ Object {
+ "data": Array [
+ Object {
+ "x": 2016-10-27T14:33:50.000Z,
+ "y": 5,
+ },
+ Object {
+ "x": 2016-10-27T10:21:15.000Z,
+ "y": 16,
+ },
+ Object {
+ "x": 2016-10-26T10:17:29.000Z,
+ "y": 12,
+ },
+ ],
+ "name": "bugs",
+ "style": 0,
+ "translatedName": "metric.bugs.name",
+ },
+ ]
+ }
+ />
+ <div
+ className="project-activity-graph"
+ >
+ <AutoSizer
+ onResize={[Function]}
+ />
+ </div>
+</div>
+`;
+
+exports[`should show a loading view 1`] = `
+<div
+ className="project-activity-graph-container"
+>
+ <div
+ className="text-center"
+ >
+ <i
+ className="spinner"
+ />
+ </div>
+</div>
+`;
+
+exports[`should show that there is no data 1`] = `
+<div
+ className="project-activity-graph-container"
+>
+ <div
+ className="note text-center"
+ >
+ component_measures.no_history
+ </div>
+</div>
+`;
--- /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="big-spacer-left big-spacer-right"
+ >
+ <ChartLegendIcon
+ className="spacer-right line-chart-legend line-chart-legend-2"
+ />
+ Bugs
+ </span>
+ <span
+ className="big-spacer-left big-spacer-right"
+ >
+ <ChartLegendIcon
+ className="spacer-right line-chart-legend line-chart-legend-1"
+ />
+ Code Smells
+ </span>
+</div>
+`;
},
]
}
+ metrics={
+ Array [
+ Object {
+ "key": "code_smells",
+ "name": "Code Smells",
+ "type": "INT",
+ },
+ ]
+ }
metricsType="INT"
project="org.sonarsource.sonarqube:sonarqube"
query={
graph="overview"
updateQuery={[Function]}
/>
- <StaticGraphs
+ <GraphsHistory
analyses={
Array [
Object {
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should correctly filter events 1`] = `
-Array [
- Object {
- "className": "VERSION",
- "date": 2016-10-26T10:17:29.000Z,
- "name": "6.4",
- },
- Object {
- "className": "VERSION",
- "date": 2016-10-27T14:33:50.000Z,
- "name": "6.5-SNAPSHOT",
- },
-]
-`;
-
-exports[`should correctly filter events 2`] = `
-Array [
- Object {
- "className": "OTHER",
- "date": 2016-10-26T10:17:29.000Z,
- "name": "foo",
- },
-]
-`;
-
-exports[`should correctly render a graph 1`] = `
-<div
- className="project-activity-graph-container"
->
- <StaticGraphsLegend
- series={
- Array [
- Object {
- "data": Array [
- Object {
- "x": 2016-10-27T14:33:50.000Z,
- "y": 5,
- },
- Object {
- "x": 2016-10-27T10:21:15.000Z,
- "y": 16,
- },
- Object {
- "x": 2016-10-26T10:17:29.000Z,
- "y": 12,
- },
- ],
- "name": "bugs",
- "style": 0,
- "translatedName": "metric.bugs.name",
- },
- ]
- }
- />
- <div
- className="project-activity-graph"
- >
- <AutoSizer
- onResize={[Function]}
- />
- </div>
-</div>
-`;
-
-exports[`should show a loading view 1`] = `
-<div
- className="project-activity-graph-container"
->
- <div
- className="text-center"
- >
- <i
- className="spinner"
- />
- </div>
-</div>
-`;
-
-exports[`should show that there is no data 1`] = `
-<div
- className="project-activity-graph-container"
->
- <div
- className="note text-center"
- >
- component_measures.no_history
- </div>
-</div>
-`;
+++ /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="big-spacer-left big-spacer-right"
- >
- <ChartLegendIcon
- className="spacer-right line-chart-legend line-chart-legend-2"
- />
- Bugs
- </span>
- <span
- className="big-spacer-left big-spacer-right"
- >
- <ChartLegendIcon
- className="spacer-right line-chart-legend line-chart-legend-1"
- />
- Code Smells
- </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.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import Select from 'react-select';
+import { translate } from '../../../../helpers/l10n';
+import type { Metric } from '../../types';
+
+type Props = {
+ addMetric: (metric: string) => void,
+ className?: string,
+ metrics: Array<Metric>,
+ selectedMetrics: Array<string>
+};
+
+type State = {
+ open: boolean,
+ selectedMetric?: string
+};
+
+export default class AddGraphMetric extends React.PureComponent {
+ props: Props;
+ state: State = {
+ open: false
+ };
+
+ getMetricsType = () => {
+ if (this.props.selectedMetrics.length > 0) {
+ const metric = this.props.metrics.find(
+ metric => metric.key === this.props.selectedMetrics[0]
+ );
+ return metric && metric.type;
+ }
+ };
+
+ getMetricsOptions = () => {
+ const selectedType = this.getMetricsType();
+ return this.props.metrics
+ .filter(metric => {
+ if (metric.hidden) {
+ return false;
+ }
+ if (selectedType) {
+ return selectedType === metric.type && !this.props.selectedMetrics.includes(metric.key);
+ }
+ return true;
+ })
+ .map((metric: Metric) => ({
+ value: metric.key,
+ label: metric.custom ? metric.name : translate('metric', metric.key, 'name')
+ }));
+ };
+
+ openForm = () => {
+ this.setState({
+ open: true
+ });
+ };
+
+ closeForm = () => {
+ this.setState({
+ open: false,
+ selectedMetric: undefined
+ });
+ };
+
+ handleChange = (option: { value: string, label: string }) =>
+ this.setState({ selectedMetric: option.value });
+
+ handleSubmit = (e: Object) => {
+ e.preventDefault();
+ if (this.state.selectedMetric) {
+ this.props.addMetric(this.state.selectedMetric);
+ this.closeForm();
+ }
+ };
+
+ renderModal() {
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel="graph metric add"
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.closeForm}>
+ <header className="modal-head">
+ <h2>{translate('project_activity.graphs.custom.add_metric')}</h2>
+ </header>
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ <div className="modal-large-field">
+ <label>{translate('project_activity.graphs.custom.search')}</label>
+ <Select
+ autofocus={true}
+ className="Select-big"
+ clearable={false}
+ noResultsText={translate('no_results')}
+ onChange={this.handleChange}
+ options={this.getMetricsOptions()}
+ placeholder=""
+ searchable={true}
+ value={this.state.selectedMetric}
+ />
+ </div>
+ </div>
+ <footer className="modal-foot">
+ <div>
+ <button type="submit" disabled={!this.state.selectedMetric}>
+ {translate('project_activity.graphs.custom.add')}
+ </button>
+ <button type="reset" className="button-link" onClick={this.closeForm}>
+ {translate('cancel')}
+ </button>
+ </div>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+
+ render() {
+ return (
+ <button className={this.props.className} onClick={this.openForm}>
+ {translate('project_activity.graphs.custom.add')}
+ {this.state.open && this.renderModal()}
+ </button>
+ );
+ }
+}
export type MeasureHistory = { metric: string, history: Array<HistoryItem> };
export type Metric = {
+ custom?: boolean,
+ hidden?: boolean,
key: string,
name: string,
type: string
return previousFrom !== nextFrom || previousTo !== nextTo;
};
+export const hasHistoryData = (series: Array<Serie>) =>
+ series.some(
+ serie => serie.data && serie.data.length > 2 && serie.data.some(p => p.y || p.y === 0)
+ );
+
export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
prevQuery.graph !== nextQuery.graph;
+++ /dev/null
-.Select-big .Select-control {
- padding-top: 4px;
- padding-bottom: 4px;
-}
-
-.Select-big .Select-placeholder {
- margin-top: 4px;
- margin-bottom: 4px;
-}
-
-.Select-big .Select-value-label {
- margin-top: 5px;
-}
import { translate, translateWithParameters } from '../../../helpers/l10n';
import UsersSelectSearchOption from './UsersSelectSearchOption';
import UsersSelectSearchValue from './UsersSelectSearchValue';
-import './UsersSelectSearch.css';
export type Option = {
login: string,
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
title={user.name}>
- <div className="little-spacer-bottom little-spacer-top">
- <Avatar hash={user.avatar} email={user.email} name={user.name} size={AVATAR_SIZE} />
- <strong className="spacer-left">{this.props.children}</strong>
- <span className="note little-spacer-left">{user.login}</span>
- </div>
+ <Avatar hash={user.avatar} email={user.email} name={user.name} size={AVATAR_SIZE} />
+ <strong className="spacer-left">{this.props.children}</strong>
+ <span className="note little-spacer-left">{user.login}</span>
</div>
);
}
onMouseMove={[Function]}
title="Administrator"
>
- <div
- className="little-spacer-bottom little-spacer-top"
+ <Connect(Avatar)
+ email="admin@admin.ch"
+ name="Administrator"
+ size={20}
+ />
+ <strong
+ className="spacer-left"
>
- <Connect(Avatar)
- email="admin@admin.ch"
- name="Administrator"
- size={20}
- />
- <strong
- className="spacer-left"
- >
- Administrator
- </strong>
- <span
- className="note little-spacer-left"
- >
- admin
- </span>
- </div>
+ Administrator
+ </strong>
+ <span
+ className="note little-spacer-left"
+ >
+ admin
+ </span>
</div>
`;
onMouseMove={[Function]}
title="Administrator"
>
- <div
- className="little-spacer-bottom little-spacer-top"
+ <Connect(Avatar)
+ hash="7daf6c79d4802916d83f6266e24850af"
+ name="Administrator"
+ size={20}
+ />
+ <strong
+ className="spacer-left"
+ >
+ Administrator
+ </strong>
+ <span
+ className="note little-spacer-left"
>
- <Connect(Avatar)
- hash="7daf6c79d4802916d83f6266e24850af"
- name="Administrator"
- size={20}
- />
- <strong
- className="spacer-left"
- >
- Administrator
- </strong>
- <span
- className="note little-spacer-left"
- >
- admin
- </span>
- </div>
+ admin
+ </span>
</div>
`;
} else if (props.metricType === 'LEVEL') {
return this.getLevelScale(availableHeight);
} else {
- return scaleLinear().range([availableHeight, 0]).domain([0, max(flatData, d => d.y)]).nice();
+ return scaleLinear()
+ .range([availableHeight, 0])
+ .domain([0, max(flatData, d => d.y) || 0])
+ .nice();
}
};
opacity: 0.5;
}
+.Select-big .Select-control {
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+
+.Select-big .Select-placeholder {
+ margin-top: 4px;
+ margin-bottom: 4px;
+}
+
+.Select-big .Select-value-label {
+ display: inline-block;
+ margin-top: 5px;
+}
+
+.Select-big .Select-option {
+ padding: 4px 8px;
+}
+
+.Select-big img {
+ padding-top: 0;
+}
+
.Select--multi .Select-value-icon,
.Select--multi .Select-value-label {
display: inline-block;
project_activity.graphs.coverage=Coverage
project_activity.graphs.duplications=Duplications
project_activity.graphs.custom=Custom
+project_activity.graphs.custom.add=Add metric
+project_activity.graphs.custom.add_metric=Add a metric
project_activity.graphs.custom.no_history=There is no historical data to show, please add more metrics to your graph.
+project_activity.graphs.custom.search=Search for a metric by name
project_activity.custom_metric.covered_lines=Covered Lines