export const getAllTimeMachineData = (
component: string,
metrics: Array<string>,
- other?: { p?: number, ps?: number, from?: string, to?: string },
+ other?: { p?: number, from?: string, to?: string },
prev?: Response
): Promise<Response> =>
- getTimeMachineData(component, metrics, other).then((r: Response) => {
+ getTimeMachineData(component, metrics, { ...other, ps: 1000 }).then((r: Response) => {
const result = prev
? {
measures: prev.measures.map((measure, idx) => ({
}
: r;
- if (
- // TODO Remove the sameAsPrevious condition when the webservice paging is working correctly ?
- // Or keep it to be sure to not have an infinite loop ?
- result.measures.every((measure, idx) => {
- const equalToTotal = measure.history.length >= result.paging.total;
- const sameAsPrevious = prev && measure.history.length === prev.measures[idx].history.length;
- return equalToTotal || sameAsPrevious;
- })
- ) {
+ if (result.paging.pageIndex * result.paging.pageSize >= result.paging.total) {
return result;
}
return getAllTimeMachineData(
const emptyState = {
analyses: [],
+ analysesLoading: false,
+ graphLoading: false,
loading: false,
measuresHistory: [],
measures: [],
*/
// @flow
import type { Event } from './types';
-import type { State } from './components/ProjectActivityApp';
+import type { State } from './components/ProjectActivityAppContainer';
export const addCustomEvent = (analysis: string, event: Event) => (state: State) => ({
analyses: state.analyses.map(item => {
import { groupBy } from 'lodash';
import moment from 'moment';
import ProjectActivityAnalysis from './ProjectActivityAnalysis';
-import ProjectActivityPageFooter from './ProjectActivityPageFooter';
import FormattedDate from '../../../components/ui/FormattedDate';
import { translate } from '../../../helpers/l10n';
-import type { Analysis, Paging } from '../types';
+import type { Analysis } from '../types';
type Props = {
addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>,
addVersion: (analysis: string, version: string) => Promise<*>,
analyses: Array<Analysis>,
+ analysesLoading: boolean,
canAdmin: boolean,
className?: string,
changeEvent: (event: string, name: string) => Promise<*>,
deleteAnalysis: (analysis: string) => Promise<*>,
deleteEvent: (analysis: string, event: string) => Promise<*>,
- fetchMoreActivity: () => void,
- loading: boolean,
- paging?: Paging
+ loading: boolean
};
export default function ProjectActivityAnalysesList(props: Props) {
</ul>
</li>
))}
+ {props.analysesLoading && <li className="text-center"><i className="spinner" /></li>}
</ul>
-
- <ProjectActivityPageFooter
- analyses={props.analyses}
- fetchMoreActivity={props.fetchMoreActivity}
- paging={props.paging}
- />
</div>
);
}
import ProjectActivityPageHeader from './ProjectActivityPageHeader';
import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
import ProjectActivityGraphs from './ProjectActivityGraphs';
-import throwGlobalError from '../../../app/utils/throwGlobalError';
-import * as api from '../../../api/projectActivity';
-import * as actions from '../actions';
-import { getAllTimeMachineData } from '../../../api/time-machine';
-import { getMetrics } from '../../../api/metrics';
-import { GRAPHS_METRICS, parseQuery, serializeQuery, serializeUrlQuery } from '../utils';
+import { GRAPHS_METRICS, activityQueryChanged } from '../utils';
import { translate } from '../../../helpers/l10n';
import './projectActivity.css';
-import type { Analysis, MeasureHistory, Metric, Query, Paging } from '../types';
-import type { RawQuery } from '../../../helpers/query';
+import type { Analysis, MeasureHistory, Metric, Query } from '../types';
type Props = {
- location: { pathname: string, query: RawQuery },
- project: { configuration?: { showHistory: boolean }, key: string, leakPeriodDate: string },
- router: { push: ({ pathname: string, query?: RawQuery }) => void }
-};
-
-export type State = {
+ addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>,
+ addVersion: (analysis: string, version: string) => Promise<*>,
analyses: Array<Analysis>,
+ analysesLoading: boolean,
+ changeEvent: (event: string, name: string) => Promise<*>,
+ deleteAnalysis: (analysis: string) => Promise<*>,
+ deleteEvent: (analysis: string, event: string) => Promise<*>,
loading: boolean,
- measures: Array<*>,
+ project: { configuration?: { showHistory: boolean }, key: string, leakPeriodDate: string },
metrics: Array<Metric>,
measuresHistory: Array<MeasureHistory>,
- paging?: Paging,
- query: Query
+ query: Query,
+ updateQuery: (newQuery: Query) => void
+};
+
+type State = {
+ filteredAnalyses: Array<Analysis>
};
export default class ProjectActivityApp extends React.PureComponent {
- mounted: boolean;
props: Props;
state: State;
constructor(props: Props) {
super(props);
- this.state = {
- analyses: [],
- loading: true,
- measures: [],
- measuresHistory: [],
- metrics: [],
- query: parseQuery(props.location.query)
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- this.handleQueryChange();
- const elem = document.querySelector('html');
- elem && elem.classList.add('dashboard-page');
+ this.state = { filteredAnalyses: this.filterAnalyses(props.analyses, props.query) };
}
- componentDidUpdate(prevProps: Props) {
- if (prevProps.location.query !== this.props.location.query) {
- this.handleQueryChange();
+ componentWillReceiveProps(nextProps: Props) {
+ if (
+ nextProps.analyses !== this.props.analyses ||
+ activityQueryChanged(this.props.query, nextProps.query)
+ ) {
+ this.setState({
+ filteredAnalyses: this.filterAnalyses(nextProps.analyses, nextProps.query)
+ });
}
}
- componentWillUnmount() {
- this.mounted = false;
- const elem = document.querySelector('html');
- elem && elem.classList.remove('dashboard-page');
- }
-
- fetchActivity = (
- query: Query,
- additional?: {}
- ): Promise<{ analyses: Array<Analysis>, paging: Paging }> => {
- const parameters = {
- ...serializeQuery(query),
- ...additional
- };
- return api.getProjectActivity(parameters).catch(throwGlobalError);
- };
-
- fetchMetrics = (): Promise<Array<Metric>> => getMetrics().catch(throwGlobalError);
-
- fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> =>
- getAllTimeMachineData(this.props.project.key, metrics).then(
- ({ measures }) =>
- measures.map(measure => ({
- metric: measure.metric,
- history: measure.history.map(analysis => ({
- date: moment(analysis.date).toDate(),
- value: analysis.value
- }))
- })),
- throwGlobalError
- );
-
- fetchMoreActivity = () => {
- const { paging, query } = this.state;
- if (!paging) {
- return;
+ filterAnalyses = (analyses: Array<Analysis>, query: Query): Array<Analysis> => {
+ if (!query.category) {
+ return analyses;
}
-
- this.setState({ loading: true });
- this.fetchActivity(query, { p: paging.pageIndex + 1 }).then(({ analyses, paging }) => {
- if (this.mounted) {
- this.setState((state: State) => ({
- analyses: state.analyses ? state.analyses.concat(analyses) : analyses,
- loading: false,
- paging
- }));
- }
- });
+ return analyses.filter(
+ analysis => analysis.events.find(event => event.category === query.category) != null
+ );
};
- addCustomEvent = (analysis: string, name: string, category?: string): Promise<*> =>
- api
- .createEvent(analysis, name, category)
- .then(
- ({ analysis, ...event }) =>
- this.mounted && this.setState(actions.addCustomEvent(analysis, event)),
- throwGlobalError
- );
-
- addVersion = (analysis: string, version: string): Promise<*> =>
- this.addCustomEvent(analysis, version, 'VERSION');
-
- deleteEvent = (analysis: string, event: string): Promise<*> =>
- api
- .deleteEvent(event)
- .then(
- () => this.mounted && this.setState(actions.deleteEvent(analysis, event)),
- throwGlobalError
- );
-
- changeEvent = (event: string, name: string): Promise<*> =>
- api
- .changeEvent(event, name)
- .then(
- ({ analysis, ...event }) =>
- this.mounted && this.setState(actions.changeEvent(analysis, event)),
- throwGlobalError
- );
-
- deleteAnalysis = (analysis: string): Promise<*> =>
- api
- .deleteAnalysis(analysis)
- .then(
- () => this.mounted && this.setState(actions.deleteAnalysis(analysis)),
- throwGlobalError
- );
-
getMetricType = () => {
- const metricKey = GRAPHS_METRICS[this.state.query.graph][0];
- const metric = this.state.metrics.find(metric => metric.key === metricKey);
+ const metricKey = GRAPHS_METRICS[this.props.query.graph][0];
+ const metric = this.props.metrics.find(metric => metric.key === metricKey);
return metric ? metric.type : 'INT';
};
- handleQueryChange() {
- const query = parseQuery(this.props.location.query);
- const graphMetrics = GRAPHS_METRICS[query.graph];
- this.setState({ loading: true, query });
-
- Promise.all([
- this.fetchActivity(query),
- this.fetchMetrics(),
- this.fetchMeasuresHistory(graphMetrics)
- ]).then(response => {
- if (this.mounted) {
- this.setState({
- analyses: response[0].analyses,
- loading: false,
- metrics: response[1],
- measuresHistory: response[2],
- paging: response[0].paging
- });
- }
- });
- }
-
- updateQuery = (newQuery: Query) => {
- this.props.router.push({
- pathname: this.props.location.pathname,
- query: {
- ...serializeUrlQuery({
- ...this.state.query,
- ...newQuery
- }),
- id: this.props.project.key
- }
- });
- };
-
render() {
- const { analyses, loading, query } = this.state;
+ const { loading, measuresHistory, query } = this.props;
+ const { filteredAnalyses } = this.state;
const { configuration } = this.props.project;
const canAdmin = configuration ? configuration.showHistory : false;
return (
<div id="project-activity" className="page page-limited">
<Helmet title={translate('project_activity.page')} />
- <ProjectActivityPageHeader category={query.category} updateQuery={this.updateQuery} />
+ <ProjectActivityPageHeader category={query.category} updateQuery={this.props.updateQuery} />
<div className="layout-page project-activity-page">
<div className="layout-page-side-outer project-activity-page-side-outer boxed-group">
<ProjectActivityAnalysesList
- addCustomEvent={this.addCustomEvent}
- addVersion={this.addVersion}
- analyses={analyses}
+ addCustomEvent={this.props.addCustomEvent}
+ addVersion={this.props.addVersion}
+ analysesLoading={this.props.analysesLoading}
+ analyses={filteredAnalyses}
canAdmin={canAdmin}
className="boxed-group-inner"
- changeEvent={this.changeEvent}
- deleteAnalysis={this.deleteAnalysis}
- deleteEvent={this.deleteEvent}
- fetchMoreActivity={this.fetchMoreActivity}
+ changeEvent={this.props.changeEvent}
+ deleteAnalysis={this.props.deleteAnalysis}
+ deleteEvent={this.props.deleteEvent}
loading={loading}
- paging={this.state.paging}
/>
</div>
<div className="project-activity-layout-page-main">
<ProjectActivityGraphs
- analyses={analyses}
+ analyses={filteredAnalyses}
leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()}
loading={loading}
- measuresHistory={this.state.measuresHistory}
+ measuresHistory={measuresHistory}
metricsType={this.getMetricType()}
project={this.props.project.key}
query={query}
- updateQuery={this.updateQuery}
+ updateQuery={this.props.updateQuery}
/>
</div>
</div>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
+import React from 'react';
+import moment from 'moment';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import ProjectActivityApp from './ProjectActivityApp';
+import throwGlobalError from '../../../app/utils/throwGlobalError';
import { getComponent } from '../../../store/rootReducer';
+import { getAllTimeMachineData } from '../../../api/time-machine';
+import { getMetrics } from '../../../api/metrics';
+import * as api from '../../../api/projectActivity';
+import * as actions from '../actions';
+import { GRAPHS_METRICS, parseQuery, serializeQuery, serializeUrlQuery } from '../utils';
+import type { RawQuery } from '../../../helpers/query';
+import type { Analysis, MeasureHistory, Metric, Paging, Query } from '../types';
+
+type Props = {
+ location: { pathname: string, query: RawQuery },
+ project: { configuration?: { showHistory: boolean }, key: string, leakPeriodDate: string },
+ router: { push: ({ pathname: string, query?: RawQuery }) => void }
+};
+
+export type State = {
+ analyses: Array<Analysis>,
+ analysesLoading: boolean,
+ graphLoading: boolean,
+ loading: boolean,
+ metrics: Array<Metric>,
+ measuresHistory: Array<MeasureHistory>,
+ paging?: Paging,
+ query: Query
+};
+
+class ProjectActivityAppContainer extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ analyses: [],
+ analysesLoading: false,
+ graphLoading: true,
+ loading: true,
+ measuresHistory: [],
+ metrics: [],
+ query: parseQuery(props.location.query)
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.firstLoadData();
+ const elem = document.querySelector('html');
+ elem && elem.classList.add('dashboard-page');
+ }
+
+ 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) {
+ this.updateGraphData(query.graph);
+ }
+ this.setState({ query });
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ const elem = document.querySelector('html');
+ elem && elem.classList.remove('dashboard-page');
+ }
+
+ addCustomEvent = (analysis: string, name: string, category?: string): Promise<*> =>
+ api
+ .createEvent(analysis, name, category)
+ .then(
+ ({ analysis, ...event }) =>
+ this.mounted && this.setState(actions.addCustomEvent(analysis, event)),
+ throwGlobalError
+ );
+
+ addVersion = (analysis: string, version: string): Promise<*> =>
+ this.addCustomEvent(analysis, version, 'VERSION');
+
+ changeEvent = (event: string, name: string): Promise<*> =>
+ api
+ .changeEvent(event, name)
+ .then(
+ ({ analysis, ...event }) =>
+ this.mounted && this.setState(actions.changeEvent(analysis, event)),
+ throwGlobalError
+ );
+
+ deleteAnalysis = (analysis: string): Promise<*> =>
+ api
+ .deleteAnalysis(analysis)
+ .then(
+ () => this.mounted && this.setState(actions.deleteAnalysis(analysis)),
+ throwGlobalError
+ );
+
+ deleteEvent = (analysis: string, event: string): Promise<*> =>
+ api
+ .deleteEvent(event)
+ .then(
+ () => this.mounted && this.setState(actions.deleteEvent(analysis, event)),
+ throwGlobalError
+ );
+
+ fetchActivity = (
+ project: string,
+ p: number,
+ ps: number,
+ additional?: {
+ [string]: string
+ }
+ ): Promise<{ analyses: Array<Analysis>, paging: Paging }> => {
+ const parameters = { project, p, ps };
+ return api.getProjectActivity({ ...parameters, ...additional }).catch(throwGlobalError);
+ };
+
+ fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> =>
+ getAllTimeMachineData(this.props.project.key, metrics).then(
+ ({ measures }) =>
+ measures.map(measure => ({
+ metric: measure.metric,
+ history: measure.history.map(analysis => ({
+ date: moment(analysis.date).toDate(),
+ value: analysis.value
+ }))
+ })),
+ throwGlobalError
+ );
+
+ fetchMetrics = (): Promise<Array<Metric>> => getMetrics().catch(throwGlobalError);
+
+ loadAllActivities = (
+ project: string,
+ prevResult?: { analyses: Array<Analysis>, paging: Paging }
+ ): Promise<{ analyses: Array<Analysis>, paging: Paging }> => {
+ if (
+ prevResult &&
+ prevResult.paging.pageIndex * prevResult.paging.pageSize >= prevResult.paging.total
+ ) {
+ return Promise.resolve(prevResult);
+ }
+ const nextPage = prevResult ? prevResult.paging.pageIndex + 1 : 1;
+ return this.fetchActivity(project, nextPage, 500).then(result => {
+ if (!prevResult) {
+ return this.loadAllActivities(project, result);
+ }
+ return this.loadAllActivities(project, {
+ analyses: prevResult.analyses.concat(result.analyses),
+ paging: result.paging
+ });
+ });
+ };
+
+ firstLoadData() {
+ const { query } = this.state;
+ const graphMetrics = GRAPHS_METRICS[query.graph];
+ Promise.all([
+ this.fetchActivity(query.project, 1, 100, serializeQuery(query)),
+ this.fetchMetrics(),
+ this.fetchMeasuresHistory(graphMetrics)
+ ]).then(response => {
+ if (this.mounted) {
+ this.setState({
+ analyses: response[0].analyses,
+ analysesLoading: true,
+ graphLoading: false,
+ loading: false,
+ metrics: response[1],
+ measuresHistory: response[2],
+ paging: response[0].paging
+ });
+
+ this.loadAllActivities(query.project).then(({ analyses, paging }) => {
+ if (this.mounted) {
+ this.setState({
+ analyses,
+ analysesLoading: false,
+ paging
+ });
+ }
+ });
+ }
+ });
+ }
+
+ updateGraphData = (graph: string) => {
+ this.setState({ graphLoading: true });
+ return this.fetchMeasuresHistory(
+ GRAPHS_METRICS[graph]
+ ).then((measuresHistory: Array<MeasureHistory>) =>
+ this.setState({ graphLoading: false, measuresHistory })
+ );
+ };
+
+ updateQuery = (newQuery: Query) => {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeUrlQuery({
+ ...this.state.query,
+ ...newQuery
+ }),
+ id: this.props.project.key
+ }
+ });
+ };
+
+ render() {
+ return (
+ <ProjectActivityApp
+ addCustomEvent={this.addCustomEvent}
+ addVersion={this.addVersion}
+ analyses={this.state.analyses}
+ analysesLoading={this.state.analysesLoading}
+ changeEvent={this.changeEvent}
+ deleteAnalysis={this.deleteAnalysis}
+ deleteEvent={this.deleteEvent}
+ graphLoading={this.state.graphLoading}
+ loading={this.state.loading}
+ metrics={this.state.metrics}
+ measuresHistory={this.state.measuresHistory}
+ project={this.props.project}
+ query={this.state.query}
+ updateQuery={this.updateQuery}
+ />
+ );
+ }
+}
const mapStateToProps = (state, ownProps) => ({
project: getComponent(state, ownProps.location.query.id)
});
-export default connect(mapStateToProps)(withRouter(ProjectActivityApp));
+export default connect(mapStateToProps)(withRouter(ProjectActivityAppContainer));
import React from 'react';
import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
import StaticGraphs from './StaticGraphs';
-import { GRAPHS_METRICS_STYLE } from '../utils';
+import { GRAPHS_METRICS } from '../utils';
import type { RawQuery } from '../../../helpers/query';
import type { Analysis, MeasureHistory, Query } from '../types';
};
export default function ProjectActivityGraphs(props: Props) {
- const { graph } = props.query;
+ const { graph, category } = props.query;
return (
<div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
<ProjectActivityGraphsHeader graph={graph} updateQuery={props.updateQuery} />
<StaticGraphs
analyses={props.analyses}
+ eventFilter={category}
leakPeriodDate={props.leakPeriodDate}
loading={props.loading}
measuresHistory={props.measuresHistory}
metricsType={props.metricsType}
project={props.project}
- seriesStyle={GRAPHS_METRICS_STYLE[graph]}
+ seriesOrder={GRAPHS_METRICS[graph]}
showAreas={['coverage', 'duplications'].includes(graph)}
/>
</div>
// @flow
import React from 'react';
import Select from 'react-select';
+import { EVENT_TYPES } from '../utils';
import { translate } from '../../../helpers/l10n';
import type { RawQuery } from '../../../helpers/query';
};
export default class ProjectActivityPageHeader extends React.PureComponent {
+ options: Array<{ label: string, value: string }>;
props: Props;
+ constructor(props: Props) {
+ super(props);
+ this.options = EVENT_TYPES.map(category => ({
+ label: translate('event.category', category),
+ value: category
+ }));
+ }
+
handleCategoryChange = (option: ?{ value: string }) => {
this.props.updateQuery({ category: option ? option.value : '' });
};
render() {
- const selectOptions = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'].map(category => ({
- label: translate('event.category', category),
- value: category
- }));
-
return (
<header className="page-header">
<Select
clearable={true}
searchable={false}
value={this.props.category}
- options={selectOptions}
+ options={this.options}
onChange={this.handleCategoryChange}
/>
</header>
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
import StaticGraphsLegend from './StaticGraphsLegend';
import { formatMeasure, getShortType } from '../../../helpers/measures';
-import { generateCoveredLinesMetric } from '../utils';
+import { EVENT_TYPES, generateCoveredLinesMetric } from '../utils';
import { translate } from '../../../helpers/l10n';
import type { Analysis, MeasureHistory } from '../types';
type Props = {
analyses: Array<Analysis>,
+ eventFilter: string,
leakPeriodDate: Date,
loading: boolean,
measuresHistory: Array<MeasureHistory>,
metricsType: string,
- seriesStyle?: { [string]: string }
+ seriesOrder: Array<string>
};
export default class StaticGraphs extends React.PureComponent {
formatValue = value => formatMeasure(value, this.props.metricsType);
getEvents = () => {
- const events = this.props.analyses.reduce((acc, analysis) => {
- return acc.concat(
- analysis.events.map(event => ({
- className: event.category,
- name: event.name,
- date: moment(analysis.date).toDate()
- }))
- );
+ 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(events, 'date');
+ return sortBy(filteredEvents, 'date');
};
getSeries = () =>
sortBy(
- this.props.measuresHistory.map((measure, idx) => {
+ this.props.measuresHistory.map(measure => {
if (measure.metric === 'uncovered_lines') {
return generateCoveredLinesMetric(measure, this.props.measuresHistory);
}
return {
name: measure.metric,
translatedName: translate('metric', measure.metric, 'name'),
- style: this.props.seriesStyle ? this.props.seriesStyle[measure.metric] : idx,
+ style: this.props.seriesOrder.indexOf(measure.metric),
data: measure.history.map(analysis => ({
x: analysis.date,
y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value)
formatYTick={this.formatYTick}
leakPeriodDate={this.props.leakPeriodDate}
metricType={this.props.metricsType}
- padding={[25, 25, 30, 60]}
series={series}
showAreas={this.props.showAreas}
width={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 { mount, shallow } from 'enzyme';
+import ProjectActivityApp from '../ProjectActivityApp';
+
+const ANALYSES = [
+ {
+ key: 'A1',
+ date: '2016-10-27T16:33:50+0200',
+ events: [
+ {
+ key: 'E1',
+ category: 'VERSION',
+ name: '6.5-SNAPSHOT'
+ }
+ ]
+ },
+ {
+ key: 'A2',
+ date: '2016-10-27T12:21:15+0200',
+ events: []
+ },
+ {
+ key: 'A3',
+ date: '2016-10-26T12:17:29+0200',
+ events: [
+ {
+ key: 'E2',
+ category: 'VERSION',
+ name: '6.4'
+ },
+ {
+ key: 'E3',
+ category: 'OTHER',
+ name: 'foo'
+ }
+ ]
+ }
+];
+
+const DEFAULT_PROPS = {
+ addCustomEvent: () => {},
+ addVersion: () => {},
+ analyses: ANALYSES,
+ changeEvent: () => {},
+ deleteAnalysis: () => {},
+ deleteEvent: () => {},
+ loading: false,
+ project: {
+ key: 'org.sonarsource.sonarqube:sonarqube',
+ leakPeriodDate: '2017-05-16T13:50:02+0200'
+ },
+ metrics: [{ key: 'code_smells', name: 'Code Smells', type: 'INT' }],
+ measuresHistory: [
+ {
+ metric: 'code_smells',
+ history: [
+ { date: new Date('Fri Mar 04 2016 10:40:12 GMT+0100 (CET)'), value: '1749' },
+ { date: new Date('Fri Mar 04 2016 18:40:16 GMT+0100 (CET)'), value: '2286' }
+ ]
+ }
+ ],
+ query: { category: '', graph: 'overview', project: 'org.sonarsource.sonarqube:sonarqube' },
+ updateQuery: () => {}
+};
+
+it('should render correctly', () => {
+ expect(shallow(<ProjectActivityApp {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should correctly filter analyses', () => {
+ const wrapper = mount(<ProjectActivityApp {...DEFAULT_PROPS} />);
+ wrapper.setProps({ query: { ...DEFAULT_PROPS.query, category: 'VERSION' } });
+ expect(wrapper.state()).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 ProjectActivityPageHeader from '../ProjectActivityPageHeader';
+
+it('should render correctly the list of series', () => {
+ expect(
+ shallow(<ProjectActivityPageHeader category="" updateQuery={() => {}} />)
+ ).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: '2016-10-27T16:33:50+0200',
+ events: [
+ {
+ key: 'E1',
+ category: 'VERSION',
+ name: '6.5-SNAPSHOT'
+ }
+ ]
+ },
+ {
+ key: 'A2',
+ date: '2016-10-27T12:21:15+0200',
+ events: []
+ },
+ {
+ key: 'A3',
+ date: '2016-10-26T12:17:29+0200',
+ events: [
+ {
+ key: 'E2',
+ category: 'OTHER',
+ name: 'foo'
+ },
+ {
+ key: 'E3',
+ category: 'VERSION',
+ name: '6.4'
+ }
+ ]
+ }
+];
+
+const DEFAULT_PROPS = {
+ analyses: ANALYSES,
+ eventFilter: '',
+ leakPeriodDate: '2017-05-16T13:50:02+0200',
+ loading: false,
+ measuresHistory: [
+ {
+ metric: 'bugs',
+ history: [
+ { date: new Date('2016-10-27T16:33:50+0200'), value: '5' },
+ { date: new Date('2016-10-27T12:21:15+0200'), value: '16' },
+ { date: new Date('2016-10-26T12:17:29+0200'), value: '12' }
+ ]
+ }
+ ],
+ seriesOrder: ['bugs'],
+ metricsType: 'INT'
+};
+
+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} measuresHistory={[{ metric: 'bugs', history: [] }]} />)
+ ).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 analyses 1`] = `
+Object {
+ "filteredAnalyses": Array [
+ Object {
+ "date": "2016-10-27T16:33:50+0200",
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E1",
+ "name": "6.5-SNAPSHOT",
+ },
+ ],
+ "key": "A1",
+ },
+ Object {
+ "date": "2016-10-26T12:17:29+0200",
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E2",
+ "name": "6.4",
+ },
+ Object {
+ "category": "OTHER",
+ "key": "E3",
+ "name": "foo",
+ },
+ ],
+ "key": "A3",
+ },
+ ],
+}
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ className="page page-limited"
+ id="project-activity"
+>
+ <HelmetWrapper
+ title="project_activity.page"
+ />
+ <ProjectActivityPageHeader
+ category=""
+ updateQuery={[Function]}
+ />
+ <div
+ className="layout-page project-activity-page"
+ >
+ <div
+ className="layout-page-side-outer project-activity-page-side-outer boxed-group"
+ >
+ <ProjectActivityAnalysesList
+ addCustomEvent={[Function]}
+ addVersion={[Function]}
+ analyses={
+ Array [
+ Object {
+ "date": "2016-10-27T16:33:50+0200",
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E1",
+ "name": "6.5-SNAPSHOT",
+ },
+ ],
+ "key": "A1",
+ },
+ Object {
+ "date": "2016-10-27T12:21:15+0200",
+ "events": Array [],
+ "key": "A2",
+ },
+ Object {
+ "date": "2016-10-26T12:17:29+0200",
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E2",
+ "name": "6.4",
+ },
+ Object {
+ "category": "OTHER",
+ "key": "E3",
+ "name": "foo",
+ },
+ ],
+ "key": "A3",
+ },
+ ]
+ }
+ canAdmin={false}
+ changeEvent={[Function]}
+ className="boxed-group-inner"
+ deleteAnalysis={[Function]}
+ deleteEvent={[Function]}
+ loading={false}
+ />
+ </div>
+ <div
+ className="project-activity-layout-page-main"
+ >
+ <ProjectActivityGraphs
+ analyses={
+ Array [
+ Object {
+ "date": "2016-10-27T16:33:50+0200",
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E1",
+ "name": "6.5-SNAPSHOT",
+ },
+ ],
+ "key": "A1",
+ },
+ Object {
+ "date": "2016-10-27T12:21:15+0200",
+ "events": Array [],
+ "key": "A2",
+ },
+ Object {
+ "date": "2016-10-26T12:17:29+0200",
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E2",
+ "name": "6.4",
+ },
+ Object {
+ "category": "OTHER",
+ "key": "E3",
+ "name": "foo",
+ },
+ ],
+ "key": "A3",
+ },
+ ]
+ }
+ leakPeriodDate={2017-05-16T11:50:02.000Z}
+ loading={false}
+ measuresHistory={
+ Array [
+ Object {
+ "history": Array [
+ Object {
+ "date": 2016-03-04T09:40:12.000Z,
+ "value": "1749",
+ },
+ Object {
+ "date": 2016-03-04T17:40:16.000Z,
+ "value": "2286",
+ },
+ ],
+ "metric": "code_smells",
+ },
+ ]
+ }
+ metricsType="INT"
+ project="org.sonarsource.sonarqube:sonarqube"
+ query={
+ Object {
+ "category": "",
+ "graph": "overview",
+ "project": "org.sonarsource.sonarqube:sonarqube",
+ }
+ }
+ updateQuery={[Function]}
+ />
+ </div>
+ </div>
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the list of series 1`] = `
+<header
+ className="page-header"
+>
+ <Select
+ addLabelText="Add \\"{label}\\"?"
+ arrowRenderer={[Function]}
+ autosize={true}
+ backspaceRemoves={true}
+ backspaceToRemoveMessage="Press backspace to remove {label}"
+ className="input-medium"
+ clearAllText="Clear all"
+ clearValueText="Clear value"
+ clearable={true}
+ delimiter=","
+ disabled={false}
+ escapeClearsValue={true}
+ filterOptions={[Function]}
+ ignoreAccents={true}
+ ignoreCase={true}
+ inputProps={Object {}}
+ isLoading={false}
+ joinValues={false}
+ labelKey="label"
+ matchPos="any"
+ matchProp="any"
+ menuBuffer={0}
+ menuRenderer={[Function]}
+ multi={false}
+ noResultsText="No results found"
+ onBlurResetsInput={true}
+ onChange={[Function]}
+ onCloseResetsInput={true}
+ openAfterFocus={false}
+ optionComponent={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "event.category.VERSION",
+ "value": "VERSION",
+ },
+ Object {
+ "label": "event.category.QUALITY_GATE",
+ "value": "QUALITY_GATE",
+ },
+ Object {
+ "label": "event.category.QUALITY_PROFILE",
+ "value": "QUALITY_PROFILE",
+ },
+ Object {
+ "label": "event.category.OTHER",
+ "value": "OTHER",
+ },
+ ]
+ }
+ pageSize={5}
+ placeholder="project_activity.filter_events..."
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={false}
+ simpleValue={false}
+ tabSelectsValue={true}
+ value=""
+ valueComponent={[Function]}
+ valueKey="value"
+ />
+</header>
+`;
--- /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>
+`;
import type { MeasureHistory, Query } from './types';
import type { RawQuery } from '../../helpers/query';
+export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'];
export const GRAPH_TYPES = ['overview', 'coverage', 'duplications', 'remediation'];
export const GRAPHS_METRICS = {
- overview: ['bugs', 'vulnerabilities', 'code_smells'],
+ overview: ['bugs', 'code_smells', 'vulnerabilities'],
coverage: ['uncovered_lines', 'lines_to_cover'],
duplications: ['duplicated_lines', 'ncloc'],
- remediation: ['reliability_remediation_effort', 'security_remediation_effort', 'sqale_index']
-};
-export const GRAPHS_METRICS_STYLE = {
- overview: { bugs: '0', code_smells: '1', vulnerabilities: '2' },
- coverage: {
- lines_to_cover: '1',
- uncovered_lines: '0'
- },
- duplications: {
- duplicated_lines: '0',
- ncloc: '1'
- },
- remediation: {
- reliability_remediation_effort: '0',
- security_remediation_effort: '2',
- sqale_index: '1'
- }
+ remediation: ['reliability_remediation_effort', 'sqale_index', 'security_remediation_effort']
};
const parseGraph = (value?: string): string => {
});
};
+export const activityQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
+ prevQuery.category !== nextQuery.category;
+
+export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
+ prevQuery.graph !== nextQuery.graph;
+
export const generateCoveredLinesMetric = (
uncoveredLines: MeasureHistory,
measuresHistory: Array<MeasureHistory>
leakPeriodDate: Date,
padding: Array<number>,
series: Array<Serie>,
- showAreas?: boolean
+ showAreas?: boolean,
+ showEventMarkers?: boolean
};
export default class AdvancedTimeline extends React.PureComponent {
static defaultProps = {
eventSize: 8,
- padding: [10, 10, 10, 10]
+ padding: [25, 25, 30, 70]
};
getRatingScale = (availableHeight: number) =>
if (!events || !eventSize) {
return null;
}
-
+ const inRangeEvents = events.filter(
+ event => event.date >= xScale.domain()[0] && event.date <= xScale.domain()[1]
+ );
const offset = eventSize / 2;
return (
<g>
- {events.map((event, idx) => (
+ {inRangeEvents.map((event, idx) => (
<path
d={this.getEventMarker(eventSize)}
className={classNames('line-chart-event', event.className)}
key={`${idx}-${event.date.getTime()}`}
- transform={`translate(${xScale(event.date) - offset}, ${yScale.range()[0] - offset})`}
+ transform={`translate(${xScale(event.date) - offset}, ${yScale.range()[0] + offset})`}
/>
))}
</g>
{this.renderTicks(xScale, yScale)}
{this.props.showAreas && this.renderAreas(xScale, yScale)}
{this.renderLines(xScale, yScale)}
- {this.renderEvents(xScale, yScale)}
+ {this.props.showEventMarkers && this.renderEvents(xScale, yScale)}
</g>
</svg>
);
stroke-width: 2px;
&.VERSION {
- stroke: @green;
+ stroke: @blue;
}
&.QUALITY_GATE {
- stroke: @blue;
+ stroke: @green;
}
&.QUALITY_PROFILE {