import { parseDate } from '../../../helpers/dates';
import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';
import { getLeakPeriod } from '../../../helpers/periods';
-import { get } from '../../../helpers/storage';
import { METRICS, HISTORY_METRICS_LIST } from '../utils';
import {
DEFAULT_GRAPH,
getDisplayedHistoryMetrics,
- PROJECT_ACTIVITY_GRAPH,
- PROJECT_ACTIVITY_GRAPH_CUSTOM
+ getProjectActivityGraph
} from '../../projectActivity/utils';
import {
isSameBranchLike,
import { translate } from '../../../helpers/l10n';
import '../styles.css';
-interface OwnProps {
+interface Props {
branchLike?: T.BranchLike;
component: T.Component;
+ fetchMetrics: () => void;
onComponentChange: (changes: {}) => void;
-}
-
-interface StateToProps {
metrics: { [key: string]: T.Metric };
}
-interface DispatchToProps {
- fetchMetrics: () => void;
-}
-
-type Props = StateToProps & DispatchToProps & OwnProps;
-
interface State {
history?: {
[metric: string]: Array<{ date: Date; value?: string }>;
this.mounted = false;
}
- loadMeasures() {
- const { branchLike, component } = this.props;
- this.setState({ loading: true });
+ getApplicationLeakPeriod = () => {
+ return this.state.measures.find(measure => measure.metric.key === 'new_bugs')
+ ? ({ index: 1 } as T.Period)
+ : undefined;
+ };
- return getMeasuresAndMeta(component.key, METRICS, {
- additionalFields: 'metrics,periods',
- ...getBranchLikeQuery(branchLike)
- }).then(
- ({ component, metrics, periods }) => {
- if (this.mounted && metrics && component.measures) {
- this.setState({
- loading: false,
- measures: enhanceMeasuresWithMetrics(component.measures, metrics),
- periods
- });
- }
- },
- error => {
- throwGlobalError(error);
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
+ isEmpty = () => {
+ return (
+ this.state.measures === undefined ||
+ this.state.measures.find(measure => measure.metric.key === 'ncloc') === undefined
);
- }
+ };
loadHistory = () => {
const { branchLike, component } = this.props;
- const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
- let graphMetrics = getDisplayedHistoryMetrics(
- get(PROJECT_ACTIVITY_GRAPH) || 'issues',
- customGraphs ? customGraphs.split(',') : []
- );
+ const { graph, customGraphs } = getProjectActivityGraph(component.key);
+ let graphMetrics = getDisplayedHistoryMetrics(graph, customGraphs);
if (!graphMetrics || graphMetrics.length <= 0) {
graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
}
});
};
- getApplicationLeakPeriod = () =>
- this.state.measures.find(measure => measure.metric.key === 'new_bugs')
- ? ({ index: 1 } as T.Period)
- : undefined;
-
- isEmpty = () =>
- this.state.measures === undefined ||
- this.state.measures.find(measure => measure.metric.key === 'ncloc') === undefined;
+ loadMeasures = () => {
+ const { branchLike, component } = this.props;
+ this.setState({ loading: true });
- renderLoading() {
- return (
- <div className="text-center">
- <i className="spinner spinner-margin" />
- </div>
+ return getMeasuresAndMeta(component.key, METRICS, {
+ additionalFields: 'metrics,periods',
+ ...getBranchLikeQuery(branchLike)
+ }).then(
+ ({ component, metrics, periods }) => {
+ if (this.mounted && metrics && component.measures) {
+ this.setState({
+ loading: false,
+ measures: enhanceMeasuresWithMetrics(component.measures, metrics),
+ periods
+ });
+ }
+ },
+ error => {
+ throwGlobalError(error);
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
);
- }
+ };
- renderEmpty() {
+ renderEmpty = () => {
const { component } = this.props;
const isApp = component.qualifier === 'APP';
return (
</h3>
</div>
);
- }
+ };
- renderMain() {
+ renderLoading = () => {
+ return (
+ <div className="text-center">
+ <i className="spinner spinner-margin" />
+ </div>
+ );
+ };
+
+ renderMain = () => {
const { branchLike, component } = this.props;
const { periods, measures, history, historyStartDate } = this.state;
const leakPeriod =
</div>
</div>
);
- }
+ };
render() {
const { branchLike, component } = this.props;
}
}
-const mapDispatchToProps: DispatchToProps = {
- fetchMetrics
-};
+const mapDispatchToProps = { fetchMetrics };
-const mapStateToProps = (state: Store): StateToProps => ({
- metrics: getMetrics(state)
-});
+const mapStateToProps = (state: Store) => ({ metrics: getMetrics(state) });
export default connect(
mapStateToProps,
import {
getDisplayedHistoryMetrics,
DEFAULT_GRAPH,
- PROJECT_ACTIVITY_GRAPH,
- PROJECT_ACTIVITY_GRAPH_CUSTOM
+ getProjectActivityGraph
} from '../../projectActivity/utils';
import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
import { getAllTimeMachineData } from '../../../api/time-machine';
import { parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
-import { get } from '../../../helpers/storage';
interface Props {
component: string;
fetchHistory = () => {
const { component } = this.props;
- const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
- let graphMetrics = getDisplayedHistoryMetrics(
- get(PROJECT_ACTIVITY_GRAPH) || 'issues',
- customGraphs ? customGraphs.split(',') : []
- );
+ const { graph, customGraphs } = getProjectActivityGraph(component);
+ let graphMetrics = getDisplayedHistoryMetrics(graph, customGraphs);
if (!graphMetrics || graphMetrics.length <= 0) {
graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-/* eslint-disable import/first, import/order */
-jest.mock('../../../../helpers/storage', () => ({
- get: (key: string) => (key === 'sonarqube.project_activity.graph.custom' ? 'coverage' : 'custom')
-}));
-
-jest.mock('../../../../api/time-machine', () => ({
- getAllTimeMachineData: jest.fn(() =>
- Promise.resolve({
- measures: [
- {
- metric: 'coverage',
- history: [
- { date: '2017-01-01T00:00:00.000Z', value: '73' },
- { date: '2017-01-02T00:00:00.000Z', value: '82' }
- ]
- }
- ]
- })
- )
-}));
-
import * as React from 'react';
import { mount, shallow } from 'enzyme';
import Activity from '../Activity';
+import { getAllTimeMachineData } from '../../../../api/time-machine';
+import { getProjectActivityGraph } from '../../../projectActivity/utils';
+
+jest.mock('../../../projectActivity/utils', () => {
+ const utils = require.requireActual('../../../projectActivity/utils');
+ utils.getProjectActivityGraph = jest
+ .fn()
+ .mockReturnValue({ graph: 'custom', customGraphs: ['coverage'] });
+ return utils;
+});
-const getAllTimeMachineData = require('../../../../api/time-machine')
- .getAllTimeMachineData as jest.Mock<any>;
+jest.mock('../../../../api/time-machine', () => ({
+ getAllTimeMachineData: jest.fn().mockResolvedValue({
+ measures: [
+ {
+ metric: 'coverage',
+ history: [
+ { date: '2017-01-01T00:00:00.000Z', value: '73' },
+ { date: '2017-01-02T00:00:00.000Z', value: '82' }
+ ]
+ }
+ ]
+ })
+}));
beforeEach(() => {
- getAllTimeMachineData.mockClear();
+ (getAllTimeMachineData as jest.Mock).mockClear();
+ (getProjectActivityGraph as jest.Mock).mockClear();
});
it('renders', () => {
metrics: [{ key: 'coverage' }]
});
expect(wrapper).toMatchSnapshot();
+ expect(getProjectActivityGraph).toBeCalledWith('foo');
});
it('fetches history', () => {
deleteEvent: (analysis: string, event: string) => Promise<void>;
graphLoading: boolean;
initializing: boolean;
- project: Pick<T.Component, 'configuration' | 'leakPeriodDate' | 'qualifier'>;
+ project: Pick<T.Component, 'configuration' | 'key' | 'leakPeriodDate' | 'qualifier'>;
metrics: T.Metric[];
measuresHistory: MeasureHistory[];
query: Query;
loading={props.graphLoading}
measuresHistory={measuresHistory}
metrics={props.metrics}
+ project={props.project.key}
query={query}
updateQuery={props.updateQuery}
/>
import * as actions from '../actions';
import { getBranchLikeQuery } from '../../../helpers/branches';
import { parseDate } from '../../../helpers/dates';
-import { get } from '../../../helpers/storage';
import {
customMetricsChanged,
DEFAULT_GRAPH,
getHistoryMetrics,
+ getProjectActivityGraph,
isCustomGraph,
- parseQuery,
- PROJECT_ACTIVITY_GRAPH,
- PROJECT_ACTIVITY_GRAPH_CUSTOM,
- serializeQuery,
- serializeUrlQuery,
MeasureHistory,
+ parseQuery,
+ ParsedAnalysis,
Query,
- ParsedAnalysis
+ serializeQuery,
+ serializeUrlQuery
} from '../utils';
import { RawQuery } from '../../../helpers/query';
componentDidMount() {
this.mounted = true;
if (this.shouldRedirect()) {
- const newQuery = { ...this.state.query, graph: get(PROJECT_ACTIVITY_GRAPH) || 'issues' };
+ const { graph, customGraphs } = getProjectActivityGraph(this.props.component.key);
+ const newQuery = { ...this.state.query, graph };
if (isCustomGraph(newQuery.graph)) {
- const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
- newQuery.customMetrics = customGraphs ? customGraphs.split(',') : [];
+ newQuery.customMetrics = customGraphs;
}
this.props.router.replace({
pathname: this.props.location.pathname,
}
}
- componentWillReceiveProps(nextProps: Props) {
- if (nextProps.location.query !== this.props.location.query) {
- const query = parseQuery(nextProps.location.query);
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.location.query !== this.props.location.query) {
+ const query = parseQuery(this.props.location.query);
if (query.graph !== this.state.query.graph || customMetricsChanged(this.state.query, query)) {
if (this.state.initialized) {
this.updateGraphData(query.graph, query.customMetrics);
} else {
- this.firstLoadData(query, nextProps.component);
+ this.firstLoadData(query, this.props.component);
}
}
this.setState({ query });
);
};
+ fetchAllActivities = (topLevelComponent: string) => {
+ this.setState({ analysesLoading: true });
+ this.loadAllActivities(topLevelComponent).then(
+ ({ analyses, paging }) => {
+ if (this.mounted) {
+ this.setState({
+ analyses,
+ analysesLoading: false,
+ paging
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ analysesLoading: false });
+ }
+ }
+ );
+ };
+
loadAllActivities = (
project: string,
prevResult?: { analyses: ParsedAnalysis[]; paging: T.Paging }
if (this.mounted) {
this.setState({
analyses: response[0].analyses,
- analysesLoading: true,
graphLoading: false,
initialized: true,
measuresHistory: response[2],
paging: response[0].paging
});
- this.loadAllActivities(topLevelComponent).then(({ analyses, paging }) => {
- if (this.mounted) {
- this.setState({
- analyses,
- analysesLoading: false,
- paging
- });
- }
- });
+ this.fetchAllActivities(topLevelComponent);
}
},
() => {
if (this.mounted) {
- this.setState({ initialized: true, analysesLoading: false, graphLoading: false });
+ this.setState({ initialized: true, graphLoading: false });
}
}
);
key => key !== 'id' && locationQuery[key] !== ''
);
- const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
- const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
- const emptyCustomGraph =
- isCustomGraph(graph) && customGraphs && customGraphs.split(',').length <= 0;
+ const { graph, customGraphs } = getProjectActivityGraph(this.props.component.key);
+ const emptyCustomGraph = isCustomGraph(graph) && customGraphs.length <= 0;
// if there is no filter, but there are saved preferences in the localStorage
// also don't redirect to custom if there is no metrics selected for it
import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
import GraphsZoom from './GraphsZoom';
import GraphsHistory from './GraphsHistory';
-import { get, save } from '../../../helpers/storage';
+import { save } from '../../../helpers/storage';
import {
datesQueryChanged,
generateSeries,
getSeriesMetricType,
historyQueryChanged,
isCustomGraph,
+ MeasureHistory,
+ ParsedAnalysis,
+ Point,
PROJECT_ACTIVITY_GRAPH,
PROJECT_ACTIVITY_GRAPH_CUSTOM,
- splitSeriesInGraphs,
- MeasureHistory,
Query,
Serie,
- Point,
- ParsedAnalysis
+ splitSeriesInGraphs,
+ getProjectActivityGraph
} from '../utils';
interface Props {
loading: boolean;
measuresHistory: MeasureHistory[];
metrics: T.Metric[];
+ project: string;
query: Query;
updateQuery: (changes: Partial<Query>) => void;
}
this.updateQueryDateRange = debounce(this.updateQueryDateRange, 500);
}
- componentWillReceiveProps(nextProps: Props) {
+ componentDidUpdate(prevProps: Props) {
let newSeries;
let newGraphs;
if (
- nextProps.measuresHistory !== this.props.measuresHistory ||
- historyQueryChanged(this.props.query, nextProps.query)
+ prevProps.measuresHistory !== this.props.measuresHistory ||
+ historyQueryChanged(prevProps.query, this.props.query)
) {
newSeries = generateSeries(
- nextProps.measuresHistory,
- nextProps.query.graph,
- nextProps.metrics,
- getDisplayedHistoryMetrics(nextProps.query.graph, nextProps.query.customMetrics)
+ this.props.measuresHistory,
+ this.props.query.graph,
+ this.props.metrics,
+ getDisplayedHistoryMetrics(this.props.query.graph, this.props.query.customMetrics)
);
newGraphs = splitSeriesInGraphs(newSeries, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH);
}
- const newDates = this.getStateZoomDates(this.props, nextProps, newSeries);
+ const newDates = this.getStateZoomDates(prevProps, this.props, newSeries);
if (newSeries || newDates) {
let newState = {} as State;
}
}
- getStateZoomDates = (props: Props | undefined, nextProps: Props, newSeries?: Serie[]) => {
+ getStateZoomDates = (prevProps: Props | undefined, props: Props, newSeries?: Serie[]) => {
const newDates = {
- from: nextProps.query.from || undefined,
- to: nextProps.query.to || undefined
+ from: props.query.from || undefined,
+ to: props.query.to || undefined
};
- if (!props || datesQueryChanged(props.query, nextProps.query)) {
+ if (!prevProps || datesQueryChanged(prevProps.query, props.query)) {
return { graphEndDate: newDates.to, graphStartDate: newDates.from };
}
if (newDates.to === undefined && newDates.from === undefined && newSeries !== undefined) {
- const series = newSeries ? newSeries : this.state.series;
const firstValid = minBy(
- series.map(serie => serie.data.find(p => Boolean(p.y || p.y === 0))),
+ newSeries.map(serie => serie.data.find(p => Boolean(p.y || p.y === 0))),
'x'
);
const lastValid = maxBy<Point>(
- series.map(serie => findLast(serie.data, p => Boolean(p.y || p.y === 0))!),
+ newSeries.map(serie => findLast(serie.data, p => Boolean(p.y || p.y === 0))!),
'x'
);
return {
- graphEndDate: lastValid ? lastValid.x : newDates.to,
- graphStartDate: firstValid ? firstValid.x : newDates.from
+ graphEndDate: lastValid && lastValid.x,
+ graphStartDate: firstValid && firstValid.x
};
}
return null;
addCustomMetric = (metric: string) => {
const customMetrics = [...this.props.query.customMetrics, metric];
- save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','));
+ save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','), this.props.project);
this.props.updateQuery({ customMetrics });
};
removeCustomMetric = (removedMetric: string) => {
const customMetrics = this.props.query.customMetrics.filter(metric => metric !== removedMetric);
- save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','));
+ save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','), this.props.project);
this.props.updateQuery({ customMetrics });
};
updateGraph = (graph: string) => {
- save(PROJECT_ACTIVITY_GRAPH, graph);
+ save(PROJECT_ACTIVITY_GRAPH, graph, this.props.project);
if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) {
- const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
- this.props.updateQuery({ graph, customMetrics: customGraphs ? customGraphs.split(',') : [] });
+ const { customGraphs } = getProjectActivityGraph(this.props.project);
+ this.props.updateQuery({ graph, customMetrics: customGraphs });
} else {
this.props.updateQuery({ graph, customMetrics: [] });
}
graphLoading: false,
initializing: false,
project: {
+ key: 'foo',
leakPeriodDate: '2017-05-16T13:50:02+0200',
qualifier: 'TRK'
},
}
],
metrics: METRICS,
+ project: 'foo',
query: {
category: '',
customMetrics: [],
category=""
project={
Object {
+ "key": "foo",
"leakPeriodDate": "2017-05-16T13:50:02+0200",
"qualifier": "TRK",
}
initializing={false}
project={
Object {
+ "key": "foo",
"leakPeriodDate": "2017-05-16T13:50:02+0200",
"qualifier": "TRK",
}
},
]
}
+ project="foo"
query={
Object {
"category": "",
} from '../../helpers/query';
import { parseDate, startOfDay } from '../../helpers/dates';
import { getLocalizedMetricName, translate } from '../../helpers/l10n';
+import { get } from '../../helpers/storage';
export type ParsedAnalysis = T.Omit<T.Analysis, 'date'> & { date: Date };
duplications: GRAPHS_METRICS_DISPLAYED['duplications'].concat(['duplicated_lines_density'])
};
-export const PROJECT_ACTIVITY_GRAPH = 'sonarqube.project_activity.graph';
-export const PROJECT_ACTIVITY_GRAPH_CUSTOM = 'sonarqube.project_activity.graph.custom';
-
-export function datesQueryChanged(prevQuery: Query, nextQuery: Query) {
- return !isEqual(prevQuery.from, nextQuery.from) || !isEqual(prevQuery.to, nextQuery.to);
-}
+export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph';
+export const PROJECT_ACTIVITY_GRAPH_CUSTOM = 'sonar_project_activity.graph.custom';
export function activityQueryChanged(prevQuery: Query, nextQuery: Query) {
return prevQuery.category !== nextQuery.category || datesQueryChanged(prevQuery, nextQuery);
return !isEqual(prevQuery.customMetrics, nextQuery.customMetrics);
}
+export function datesQueryChanged(prevQuery: Query, nextQuery: Query) {
+ return !isEqual(prevQuery.from, nextQuery.from) || !isEqual(prevQuery.to, nextQuery.to);
+}
+
export function hasDataValues(serie: Serie) {
return serie.data.some(point => Boolean(point.y || point.y === 0));
}
return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS[graph];
}
+export function getProjectActivityGraph(project: string) {
+ const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM, project);
+ return {
+ graph: get(PROJECT_ACTIVITY_GRAPH, project) || 'issues',
+ customGraphs: customGraphs ? customGraphs.split(',') : []
+ };
+}
+
function parseGraph(value?: string) {
const graph = parseAsString(value);
return GRAPH_TYPES.includes(graph) ? graph : DEFAULT_GRAPH;
import AdvancedTimeline from '../charts/AdvancedTimeline';
import {
DEFAULT_GRAPH,
- getDisplayedHistoryMetrics,
generateSeries,
+ getDisplayedHistoryMetrics,
+ getProjectActivityGraph,
getSeriesMetricType,
hasHistoryDataValue,
- PROJECT_ACTIVITY_GRAPH,
- PROJECT_ACTIVITY_GRAPH_CUSTOM,
- splitSeriesInGraphs,
- Serie
+ Serie,
+ splitSeriesInGraphs
} from '../../apps/projectActivity/utils';
-import { get } from '../../helpers/storage';
import { formatMeasure, getShortType } from '../../helpers/measures';
import { getBranchLikeQuery } from '../../helpers/branches';
import { withRouter, Router } from '../hoc/withRouter';
class PreviewGraph extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
- const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
- const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
- const customMetrics = customGraphs ? customGraphs.split(',') : [];
+ const { graph, customGraphs: customMetrics } = getProjectActivityGraph(props.project);
const series = splitSeriesInGraphs(
this.getSeries(props.history, graph, customMetrics, props.metrics),
MAX_GRAPH_NB,
};
}
- componentWillReceiveProps(nextProps: Props) {
- if (nextProps.history !== this.props.history || nextProps.metrics !== this.props.metrics) {
- const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
- const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
- const customMetrics = customGraphs ? customGraphs.split(',') : [];
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.history !== this.props.history || prevProps.metrics !== this.props.metrics) {
+ const { graph, customGraphs: customMetrics } = getProjectActivityGraph(this.props.project);
const series = splitSeriesInGraphs(
- this.getSeries(nextProps.history, graph, customMetrics, nextProps.metrics),
+ this.getSeries(this.props.history, graph, customMetrics, this.props.metrics),
MAX_GRAPH_NB,
MAX_SERIES_PER_GRAPH
);