* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { AnalysisEvent } from '../../types/types';
-import { State } from './components/ProjectActivityAppContainer';
+import { State } from './components/ProjectActivityApp';
export function addCustomEvent(analysis: string, event: AnalysisEvent) {
return (state: State) => ({
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
-import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { useSearchParams } from 'react-router-dom';
+import { getAllMetrics } from '../../../api/metrics';
+import {
+ changeEvent,
+ createEvent,
+ deleteAnalysis,
+ deleteEvent,
+ getProjectActivity,
+ ProjectActivityStatuses
+} from '../../../api/projectActivity';
+import { getAllTimeMachineData } from '../../../api/time-machine';
+import withComponentContext from '../../../app/components/componentContext/withComponentContext';
+import {
+ DEFAULT_GRAPH,
+ getActivityGraph,
+ getHistoryMetrics,
+ isCustomGraph
+} from '../../../components/activity-graph/utils';
+import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { parseDate } from '../../../helpers/dates';
-import { translate } from '../../../helpers/l10n';
-import { MeasureHistory } from '../../../types/project-activity';
-import { Component, Metric, ParsedAnalysis } from '../../../types/types';
-import { Query } from '../utils';
-import './projectActivity.css';
-import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
-import ProjectActivityGraphs from './ProjectActivityGraphs';
-import ProjectActivityPageFilters from './ProjectActivityPageFilters';
+import { serializeStringArray } from '../../../helpers/query';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
+import { GraphType, MeasureHistory } from '../../../types/project-activity';
+import { Component, Metric, Paging, ParsedAnalysis, RawQuery } from '../../../types/types';
+import * as actions from '../actions';
+import {
+ customMetricsChanged,
+ parseQuery,
+ Query,
+ serializeQuery,
+ serializeUrlQuery
+} from '../utils';
+import ProjectActivityAppRenderer from './ProjectActivityAppRenderer';
interface Props {
- addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
- addVersion: (analysis: string, version: string) => Promise<void>;
+ branchLike?: BranchLike;
+ component: Component;
+ location: Location;
+ router: Router;
+}
+
+export interface State {
analyses: ParsedAnalysis[];
analysesLoading: boolean;
- changeEvent: (event: string, name: string) => Promise<void>;
- deleteAnalysis: (analysis: string) => Promise<void>;
- deleteEvent: (analysis: string, event: string) => Promise<void>;
graphLoading: boolean;
- initializing: boolean;
- project: Pick<Component, 'configuration' | 'key' | 'leakPeriodDate' | 'qualifier'>;
+ initialized: boolean;
metrics: Metric[];
measuresHistory: MeasureHistory[];
query: Query;
- updateQuery: (changes: Partial<Query>) => void;
}
-export default function ProjectActivityApp(props: Props) {
- const { analyses, measuresHistory, query } = props;
- const { configuration } = props.project;
- const canAdmin =
- (props.project.qualifier === 'TRK' || props.project.qualifier === 'APP') &&
- (configuration ? configuration.showHistory : false);
- const canDeleteAnalyses = configuration ? configuration.showHistory : false;
- return (
- <div className="page page-limited" id="project-activity">
- <Suggestions suggestions="project_activity" />
- <Helmet defer={false} title={translate('project_activity.page')} />
-
- <A11ySkipTarget anchor="activity_main" />
-
- <ProjectActivityPageFilters
- category={query.category}
- from={query.from}
- project={props.project}
- to={query.to}
- updateQuery={props.updateQuery}
+export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph';
+
+const ACTIVITY_PAGE_SIZE_FIRST_BATCH = 100;
+const ACTIVITY_PAGE_SIZE = 500;
+
+export class ProjectActivityApp extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ analyses: [],
+ analysesLoading: false,
+ graphLoading: true,
+ initialized: false,
+ measuresHistory: [],
+ metrics: [],
+ query: parseQuery(props.location.query)
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+
+ this.firstLoadData(this.state.query, this.props.component);
+ }
+
+ 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 || DEFAULT_GRAPH, query.customMetrics);
+ } else {
+ this.firstLoadData(query, this.props.component);
+ }
+ }
+ this.setState({ query });
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ addCustomEvent = (analysisKey: string, name: string, category?: string) => {
+ return createEvent(analysisKey, name, category).then(({ analysis, ...event }) => {
+ if (this.mounted) {
+ this.setState(actions.addCustomEvent(analysis, event));
+ }
+ });
+ };
+
+ addVersion = (analysis: string, version: string) => {
+ return this.addCustomEvent(analysis, version, 'VERSION');
+ };
+
+ changeEvent = (eventKey: string, name: string) => {
+ return changeEvent(eventKey, name).then(({ analysis, ...event }) => {
+ if (this.mounted) {
+ this.setState(actions.changeEvent(analysis, event));
+ }
+ });
+ };
+
+ deleteAnalysis = (analysis: string) => {
+ return deleteAnalysis(analysis).then(() => {
+ if (this.mounted) {
+ this.updateGraphData(
+ this.state.query.graph || DEFAULT_GRAPH,
+ this.state.query.customMetrics
+ );
+ this.setState(actions.deleteAnalysis(analysis));
+ }
+ });
+ };
+
+ deleteEvent = (analysis: string, event: string) => {
+ return deleteEvent(event).then(() => {
+ if (this.mounted) {
+ this.setState(actions.deleteEvent(analysis, event));
+ }
+ });
+ };
+
+ fetchActivity = (
+ project: string,
+ statuses: ProjectActivityStatuses[],
+ p: number,
+ ps: number,
+ additional?: RawQuery
+ ) => {
+ const parameters = {
+ project,
+ statuses: serializeStringArray(statuses),
+ p,
+ ps,
+ ...getBranchLikeQuery(this.props.branchLike)
+ };
+ return getProjectActivity({ ...additional, ...parameters }).then(({ analyses, paging }) => ({
+ analyses: analyses.map(analysis => ({
+ ...analysis,
+ date: parseDate(analysis.date)
+ })) as ParsedAnalysis[],
+ paging
+ }));
+ };
+
+ fetchMeasuresHistory = (metrics: string[]): Promise<MeasureHistory[]> => {
+ if (metrics.length <= 0) {
+ return Promise.resolve([]);
+ }
+ return getAllTimeMachineData({
+ component: this.props.component.key,
+ metrics: metrics.join(),
+ ...getBranchLikeQuery(this.props.branchLike)
+ }).then(({ measures }) =>
+ measures.map(measure => ({
+ metric: measure.metric,
+ history: measure.history.map(analysis => ({
+ date: parseDate(analysis.date),
+ value: analysis.value!
+ }))
+ }))
+ );
+ };
+
+ fetchAllActivities = (topLevelComponent: string) => {
+ this.setState({ analysesLoading: true });
+ this.loadAllActivities(topLevelComponent).then(
+ ({ analyses }) => {
+ if (this.mounted) {
+ this.setState({
+ analyses,
+ analysesLoading: false
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ analysesLoading: false });
+ }
+ }
+ );
+ };
+
+ loadAllActivities = (
+ project: string,
+ prevResult?: { analyses: ParsedAnalysis[]; paging: Paging }
+ ): Promise<{ analyses: ParsedAnalysis[]; 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,
+ [
+ ProjectActivityStatuses.STATUS_PROCESSED,
+ ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE
+ ],
+ nextPage,
+ ACTIVITY_PAGE_SIZE
+ ).then(result => {
+ if (!prevResult) {
+ return this.loadAllActivities(project, result);
+ }
+ return this.loadAllActivities(project, {
+ analyses: prevResult.analyses.concat(result.analyses),
+ paging: result.paging
+ });
+ });
+ };
+
+ getTopLevelComponent = (component: Component) => {
+ let current = component.breadcrumbs.length - 1;
+ while (
+ current > 0 &&
+ !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
+ ) {
+ current--;
+ }
+ return component.breadcrumbs[current].key;
+ };
+
+ filterMetrics({ qualifier }: Component, metrics: Metric[]) {
+ return ['VW', 'SVW'].includes(qualifier)
+ ? metrics.filter(metric => metric.key !== MetricKey.security_hotspots_reviewed)
+ : metrics.filter(metric => metric.key !== MetricKey.security_review_rating);
+ }
+
+ firstLoadData(query: Query, component: Component) {
+ const graphMetrics = getHistoryMetrics(query.graph || DEFAULT_GRAPH, query.customMetrics);
+ const topLevelComponent = this.getTopLevelComponent(component);
+ Promise.all([
+ this.fetchActivity(
+ topLevelComponent,
+ [
+ ProjectActivityStatuses.STATUS_PROCESSED,
+ ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE
+ ],
+ 1,
+ ACTIVITY_PAGE_SIZE_FIRST_BATCH,
+ serializeQuery(query)
+ ),
+ getAllMetrics(),
+ this.fetchMeasuresHistory(graphMetrics)
+ ]).then(
+ ([{ analyses }, metrics, measuresHistory]) => {
+ if (this.mounted) {
+ this.setState({
+ analyses,
+ graphLoading: false,
+ initialized: true,
+ measuresHistory,
+ metrics: this.filterMetrics(component, metrics)
+ });
+
+ this.fetchAllActivities(topLevelComponent);
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ initialized: true, graphLoading: false });
+ }
+ }
+ );
+ }
+
+ updateGraphData = (graph: GraphType, customMetrics: string[]) => {
+ const graphMetrics = getHistoryMetrics(graph, customMetrics);
+ this.setState({ graphLoading: true });
+ this.fetchMeasuresHistory(graphMetrics).then(
+ measuresHistory => {
+ if (this.mounted) {
+ this.setState({ graphLoading: false, measuresHistory });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ graphLoading: false, measuresHistory: [] });
+ }
+ }
+ );
+ };
+
+ updateQuery = (newQuery: Query) => {
+ const query = serializeUrlQuery({
+ ...this.state.query,
+ ...newQuery
+ });
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...query,
+ ...getBranchLikeQuery(this.props.branchLike),
+ id: this.props.component.key
+ }
+ });
+ };
+
+ shouldRedirect = () => {
+ const locationQuery = this.props.location.query;
+ if (!locationQuery) {
+ return false;
+ }
+ const filtered = Object.keys(locationQuery).some(
+ key => key !== 'id' && locationQuery[key] !== ''
+ );
+
+ const { graph, customGraphs } = getActivityGraph(
+ PROJECT_ACTIVITY_GRAPH,
+ 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
+ return !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
+ };
+
+ render() {
+ return (
+ <ProjectActivityAppRenderer
+ 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.initialized || this.state.graphLoading}
+ initializing={!this.state.initialized}
+ measuresHistory={this.state.measuresHistory}
+ metrics={this.state.metrics}
+ project={this.props.component}
+ query={this.state.query}
+ updateQuery={this.updateQuery}
/>
+ );
+ }
+}
+
+const isFiltered = (searchParams: URLSearchParams) => {
+ let filtered = false;
+ searchParams.forEach((value, key) => {
+ if (key !== 'id' && value !== '') {
+ filtered = true;
+ }
+ });
+ return filtered;
+};
+
+function RedirectWrapper(props: Props) {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const filtered = isFiltered(searchParams);
- <div className="layout-page project-activity-page">
- <div className="layout-page-side-outer project-activity-page-side-outer boxed-group">
- <ProjectActivityAnalysesList
- addCustomEvent={props.addCustomEvent}
- addVersion={props.addVersion}
- analyses={analyses}
- analysesLoading={props.analysesLoading}
- canAdmin={canAdmin}
- canDeleteAnalyses={canDeleteAnalyses}
- changeEvent={props.changeEvent}
- deleteAnalysis={props.deleteAnalysis}
- deleteEvent={props.deleteEvent}
- initializing={props.initializing}
- leakPeriodDate={
- props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
- }
- project={props.project}
- query={props.query}
- updateQuery={props.updateQuery}
- />
- </div>
- <div className="project-activity-layout-page-main">
- <ProjectActivityGraphs
- analyses={analyses}
- leakPeriodDate={
- props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
- }
- loading={props.graphLoading}
- measuresHistory={measuresHistory}
- metrics={props.metrics}
- project={props.project.key}
- query={query}
- updateQuery={props.updateQuery}
- />
- </div>
- </div>
- </div>
- );
+ const { graph, customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, 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
+ const shouldRedirect = !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
+
+ React.useEffect(() => {
+ if (shouldRedirect) {
+ const query = parseQuery(searchParams);
+ const newQuery = { ...query, graph };
+ if (isCustomGraph(newQuery.graph)) {
+ searchParams.set('custom_metrics', customGraphs.join(','));
+ }
+ searchParams.set('graph', graph);
+ setSearchParams(searchParams, { replace: true });
+ }
+ }, [customGraphs, graph, searchParams, setSearchParams, shouldRedirect]);
+
+ return shouldRedirect ? null : <ProjectActivityApp {...props} />;
}
+
+export default withComponentContext(withRouter(RedirectWrapper));
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 * as React from 'react';
-import { useSearchParams } from 'react-router-dom';
-import { getAllMetrics } from '../../../api/metrics';
-import {
- changeEvent,
- createEvent,
- deleteAnalysis,
- deleteEvent,
- getProjectActivity,
- ProjectActivityStatuses
-} from '../../../api/projectActivity';
-import { getAllTimeMachineData } from '../../../api/time-machine';
-import withComponentContext from '../../../app/components/componentContext/withComponentContext';
-import {
- DEFAULT_GRAPH,
- getActivityGraph,
- getHistoryMetrics,
- isCustomGraph
-} from '../../../components/activity-graph/utils';
-import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import { getBranchLikeQuery } from '../../../helpers/branch-like';
-import { parseDate } from '../../../helpers/dates';
-import { serializeStringArray } from '../../../helpers/query';
-import { BranchLike } from '../../../types/branch-like';
-import { MetricKey } from '../../../types/metrics';
-import { GraphType, MeasureHistory } from '../../../types/project-activity';
-import { Component, Metric, Paging, ParsedAnalysis, RawQuery } from '../../../types/types';
-import * as actions from '../actions';
-import {
- customMetricsChanged,
- parseQuery,
- Query,
- serializeQuery,
- serializeUrlQuery
-} from '../utils';
-import ProjectActivityApp from './ProjectActivityApp';
-
-interface Props {
- branchLike?: BranchLike;
- component: Component;
- location: Location;
- router: Router;
-}
-
-export interface State {
- analyses: ParsedAnalysis[];
- analysesLoading: boolean;
- graphLoading: boolean;
- initialized: boolean;
- metrics: Metric[];
- measuresHistory: MeasureHistory[];
- query: Query;
-}
-
-export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph';
-
-const ACTIVITY_PAGE_SIZE_FIRST_BATCH = 100;
-const ACTIVITY_PAGE_SIZE = 500;
-
-export class ProjectActivityAppContainer extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
- this.state = {
- analyses: [],
- analysesLoading: false,
- graphLoading: true,
- initialized: false,
- measuresHistory: [],
- metrics: [],
- query: parseQuery(props.location.query)
- };
- }
-
- componentDidMount() {
- this.mounted = true;
-
- this.firstLoadData(this.state.query, this.props.component);
- }
-
- 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 || DEFAULT_GRAPH, query.customMetrics);
- } else {
- this.firstLoadData(query, this.props.component);
- }
- }
- this.setState({ query });
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- addCustomEvent = (analysisKey: string, name: string, category?: string) => {
- return createEvent(analysisKey, name, category).then(({ analysis, ...event }) => {
- if (this.mounted) {
- this.setState(actions.addCustomEvent(analysis, event));
- }
- });
- };
-
- addVersion = (analysis: string, version: string) => {
- return this.addCustomEvent(analysis, version, 'VERSION');
- };
-
- changeEvent = (eventKey: string, name: string) => {
- return changeEvent(eventKey, name).then(({ analysis, ...event }) => {
- if (this.mounted) {
- this.setState(actions.changeEvent(analysis, event));
- }
- });
- };
-
- deleteAnalysis = (analysis: string) => {
- return deleteAnalysis(analysis).then(() => {
- if (this.mounted) {
- this.updateGraphData(
- this.state.query.graph || DEFAULT_GRAPH,
- this.state.query.customMetrics
- );
- this.setState(actions.deleteAnalysis(analysis));
- }
- });
- };
-
- deleteEvent = (analysis: string, event: string) => {
- return deleteEvent(event).then(() => {
- if (this.mounted) {
- this.setState(actions.deleteEvent(analysis, event));
- }
- });
- };
-
- fetchActivity = (
- project: string,
- statuses: ProjectActivityStatuses[],
- p: number,
- ps: number,
- additional?: RawQuery
- ) => {
- const parameters = {
- project,
- statuses: serializeStringArray(statuses),
- p,
- ps,
- ...getBranchLikeQuery(this.props.branchLike)
- };
- return getProjectActivity({ ...additional, ...parameters }).then(({ analyses, paging }) => ({
- analyses: analyses.map(analysis => ({
- ...analysis,
- date: parseDate(analysis.date)
- })) as ParsedAnalysis[],
- paging
- }));
- };
-
- fetchMeasuresHistory = (metrics: string[]): Promise<MeasureHistory[]> => {
- if (metrics.length <= 0) {
- return Promise.resolve([]);
- }
- return getAllTimeMachineData({
- component: this.props.component.key,
- metrics: metrics.join(),
- ...getBranchLikeQuery(this.props.branchLike)
- }).then(({ measures }) =>
- measures.map(measure => ({
- metric: measure.metric,
- history: measure.history.map(analysis => ({
- date: parseDate(analysis.date),
- value: analysis.value!
- }))
- }))
- );
- };
-
- fetchAllActivities = (topLevelComponent: string) => {
- this.setState({ analysesLoading: true });
- this.loadAllActivities(topLevelComponent).then(
- ({ analyses }) => {
- if (this.mounted) {
- this.setState({
- analyses,
- analysesLoading: false
- });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ analysesLoading: false });
- }
- }
- );
- };
-
- loadAllActivities = (
- project: string,
- prevResult?: { analyses: ParsedAnalysis[]; paging: Paging }
- ): Promise<{ analyses: ParsedAnalysis[]; 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,
- [
- ProjectActivityStatuses.STATUS_PROCESSED,
- ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE
- ],
- nextPage,
- ACTIVITY_PAGE_SIZE
- ).then(result => {
- if (!prevResult) {
- return this.loadAllActivities(project, result);
- }
- return this.loadAllActivities(project, {
- analyses: prevResult.analyses.concat(result.analyses),
- paging: result.paging
- });
- });
- };
-
- getTopLevelComponent = (component: Component) => {
- let current = component.breadcrumbs.length - 1;
- while (
- current > 0 &&
- !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
- ) {
- current--;
- }
- return component.breadcrumbs[current].key;
- };
-
- filterMetrics({ qualifier }: Component, metrics: Metric[]) {
- return ['VW', 'SVW'].includes(qualifier)
- ? metrics.filter(metric => metric.key !== MetricKey.security_hotspots_reviewed)
- : metrics.filter(metric => metric.key !== MetricKey.security_review_rating);
- }
-
- firstLoadData(query: Query, component: Component) {
- const graphMetrics = getHistoryMetrics(query.graph || DEFAULT_GRAPH, query.customMetrics);
- const topLevelComponent = this.getTopLevelComponent(component);
- Promise.all([
- this.fetchActivity(
- topLevelComponent,
- [
- ProjectActivityStatuses.STATUS_PROCESSED,
- ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE
- ],
- 1,
- ACTIVITY_PAGE_SIZE_FIRST_BATCH,
- serializeQuery(query)
- ),
- getAllMetrics(),
- this.fetchMeasuresHistory(graphMetrics)
- ]).then(
- ([{ analyses }, metrics, measuresHistory]) => {
- if (this.mounted) {
- this.setState({
- analyses,
- graphLoading: false,
- initialized: true,
- measuresHistory,
- metrics: this.filterMetrics(component, metrics)
- });
-
- this.fetchAllActivities(topLevelComponent);
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ initialized: true, graphLoading: false });
- }
- }
- );
- }
-
- updateGraphData = (graph: GraphType, customMetrics: string[]) => {
- const graphMetrics = getHistoryMetrics(graph, customMetrics);
- this.setState({ graphLoading: true });
- this.fetchMeasuresHistory(graphMetrics).then(
- measuresHistory => {
- if (this.mounted) {
- this.setState({ graphLoading: false, measuresHistory });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ graphLoading: false, measuresHistory: [] });
- }
- }
- );
- };
-
- updateQuery = (newQuery: Query) => {
- const query = serializeUrlQuery({
- ...this.state.query,
- ...newQuery
- });
- this.props.router.push({
- pathname: this.props.location.pathname,
- query: {
- ...query,
- ...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component.key
- }
- });
- };
-
- shouldRedirect = () => {
- const locationQuery = this.props.location.query;
- if (!locationQuery) {
- return false;
- }
- const filtered = Object.keys(locationQuery).some(
- key => key !== 'id' && locationQuery[key] !== ''
- );
-
- const { graph, customGraphs } = getActivityGraph(
- PROJECT_ACTIVITY_GRAPH,
- 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
- return !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
- };
-
- 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.initialized || this.state.graphLoading}
- initializing={!this.state.initialized}
- measuresHistory={this.state.measuresHistory}
- metrics={this.state.metrics}
- project={this.props.component}
- query={this.state.query}
- updateQuery={this.updateQuery}
- />
- );
- }
-}
-
-const isFiltered = (searchParams: URLSearchParams) => {
- let filtered = false;
- searchParams.forEach((value, key) => {
- if (key !== 'id' && value !== '') {
- filtered = true;
- }
- });
- return filtered;
-};
-
-function RedirectWrapper(props: Props) {
- const [searchParams, setSearchParams] = useSearchParams();
-
- const filtered = isFiltered(searchParams);
-
- const { graph, customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, 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
- const shouldRedirect = !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
-
- React.useEffect(() => {
- if (shouldRedirect) {
- const query = parseQuery(searchParams);
- const newQuery = { ...query, graph };
- if (isCustomGraph(newQuery.graph)) {
- searchParams.set('custom_metrics', customGraphs.join(','));
- }
- searchParams.set('graph', graph);
- setSearchParams(searchParams, { replace: true });
- }
- }, [customGraphs, graph, searchParams, setSearchParams, shouldRedirect]);
-
- return shouldRedirect ? null : <ProjectActivityAppContainer {...props} />;
-}
-
-export default withComponentContext(withRouter(RedirectWrapper));
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { parseDate } from '../../../helpers/dates';
+import { translate } from '../../../helpers/l10n';
+import { MeasureHistory } from '../../../types/project-activity';
+import { Component, Metric, ParsedAnalysis } from '../../../types/types';
+import { Query } from '../utils';
+import './projectActivity.css';
+import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
+import ProjectActivityGraphs from './ProjectActivityGraphs';
+import ProjectActivityPageFilters from './ProjectActivityPageFilters';
+
+interface Props {
+ addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
+ addVersion: (analysis: string, version: string) => Promise<void>;
+ analyses: ParsedAnalysis[];
+ analysesLoading: boolean;
+ changeEvent: (event: string, name: string) => Promise<void>;
+ deleteAnalysis: (analysis: string) => Promise<void>;
+ deleteEvent: (analysis: string, event: string) => Promise<void>;
+ graphLoading: boolean;
+ initializing: boolean;
+ project: Pick<Component, 'configuration' | 'key' | 'leakPeriodDate' | 'qualifier'>;
+ metrics: Metric[];
+ measuresHistory: MeasureHistory[];
+ query: Query;
+ updateQuery: (changes: Partial<Query>) => void;
+}
+
+export default function ProjectActivityAppRenderer(props: Props) {
+ const { analyses, measuresHistory, query } = props;
+ const { configuration } = props.project;
+ const canAdmin =
+ (props.project.qualifier === 'TRK' || props.project.qualifier === 'APP') &&
+ (configuration ? configuration.showHistory : false);
+ const canDeleteAnalyses = configuration ? configuration.showHistory : false;
+ return (
+ <div className="page page-limited" id="project-activity">
+ <Suggestions suggestions="project_activity" />
+ <Helmet defer={false} title={translate('project_activity.page')} />
+
+ <A11ySkipTarget anchor="activity_main" />
+
+ <ProjectActivityPageFilters
+ category={query.category}
+ from={query.from}
+ project={props.project}
+ to={query.to}
+ updateQuery={props.updateQuery}
+ />
+
+ <div className="layout-page project-activity-page">
+ <div className="layout-page-side-outer project-activity-page-side-outer boxed-group">
+ <ProjectActivityAnalysesList
+ addCustomEvent={props.addCustomEvent}
+ addVersion={props.addVersion}
+ analyses={analyses}
+ analysesLoading={props.analysesLoading}
+ canAdmin={canAdmin}
+ canDeleteAnalyses={canDeleteAnalyses}
+ changeEvent={props.changeEvent}
+ deleteAnalysis={props.deleteAnalysis}
+ deleteEvent={props.deleteEvent}
+ initializing={props.initializing}
+ leakPeriodDate={
+ props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
+ }
+ project={props.project}
+ query={props.query}
+ updateQuery={props.updateQuery}
+ />
+ </div>
+ <div className="project-activity-layout-page-main">
+ <ProjectActivityGraphs
+ analyses={analyses}
+ leakPeriodDate={
+ props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
+ }
+ loading={props.graphLoading}
+ measuresHistory={measuresHistory}
+ metrics={props.metrics}
+ project={props.project.key}
+ query={query}
+ updateQuery={props.updateQuery}
+ />
+ </div>
+ </div>
+ </div>
+ );
+}
import { GraphType, MeasureHistory, Point, Serie } from '../../../types/project-activity';
import { Metric, ParsedAnalysis } from '../../../types/types';
import { datesQueryChanged, historyQueryChanged, Query } from '../utils';
-import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityAppContainer';
+import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityApp';
interface Props {
analyses: ParsedAnalysis[];
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
+import React from 'react';
+import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext';
+import { getActivityGraph } from '../../../../components/activity-graph/utils';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { Component } from '../../../../types/types';
+import ProjectActivityAppContainer from '../ProjectActivityApp';
+
+jest.mock('../../../../api/time-machine', () => {
+ const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ getAllTimeMachineData: jest.fn().mockResolvedValue({
+ measures: [
+ {
+ metric: 'bugs',
+ history: [{ date: '2022-01-01', value: '10' }]
+ }
+ ],
+ paging: mockPaging({ total: 1 })
+ })
+ };
+});
+
+jest.mock('../../../../api/metrics', () => {
+ const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ getAllMetrics: jest.fn().mockResolvedValue([mockMetric()])
+ };
+});
+
+jest.mock('../../../../api/projectActivity', () => {
+ const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ ...jest.requireActual('../../../../api/projectActivity'),
+ createEvent: jest.fn(),
+ changeEvent: jest.fn(),
+ getProjectActivity: jest.fn().mockResolvedValue({
+ analyses: [mockAnalysis({ key: 'foo' })],
+ paging: mockPaging({ total: 1 })
+ })
+ };
+});
+
+jest.mock('../../../../components/activity-graph/utils', () => {
+ const actual = jest.requireActual('../../../../components/activity-graph/utils');
+ return {
+ ...actual,
+ getActivityGraph: jest.fn()
+ };
+});
+
+it('should render default graph', async () => {
+ (getActivityGraph as jest.Mock).mockImplementation(() => {
+ return {
+ graph: 'issues'
+ };
+ });
+
+ renderProjectActivityAppContainer();
+
+ expect(await screen.findByText('project_activity.graphs.issues')).toBeInTheDocument();
+});
+
+it('should reload custom graph from local storage', async () => {
+ (getActivityGraph as jest.Mock).mockImplementation(() => {
+ return {
+ graph: 'custom',
+ customGraphs: ['bugs', 'code_smells']
+ };
+ });
+
+ renderProjectActivityAppContainer();
+
+ expect(await screen.findByText('project_activity.graphs.custom')).toBeInTheDocument();
+});
+
+function renderProjectActivityAppContainer(
+ { component, navigateTo }: { component: Component; navigateTo?: string } = {
+ component: mockComponent({
+ breadcrumbs: [
+ { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }
+ ]
+ })
+ }
+) {
+ return renderApp(
+ 'project/activity',
+ <ComponentContext.Provider
+ value={{
+ branchLikes: [],
+ onBranchesChange: jest.fn(),
+ onComponentChange: jest.fn(),
+ component
+ }}>
+ <ProjectActivityAppContainer />
+ </ComponentContext.Provider>,
+ { navigateTo }
+ );
+}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils';
-import { parseDate } from '../../../../helpers/dates';
-import ProjectActivityApp from '../ProjectActivityApp';
+import { changeEvent, createEvent } from '../../../../api/projectActivity';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import {
+ mockAnalysisEvent,
+ mockLocation,
+ mockMetric,
+ mockRouter
+} from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { MetricKey } from '../../../../types/metrics';
+import { ProjectActivityApp } from '../ProjectActivityApp';
-const ANALYSES = [
- {
- key: 'A1',
- date: parseDate('2016-10-27T16:33:50+0200'),
- events: [
- {
- key: 'E1',
- category: 'VERSION',
- name: '6.5-SNAPSHOT'
- }
- ]
- },
- {
- key: 'A2',
- date: parseDate('2016-10-27T12:21:15+0200'),
- events: []
- },
- {
- key: 'A3',
- date: parseDate('2016-10-26T12:17:29+0200'),
- events: [
- {
- key: 'E2',
- category: 'VERSION',
- name: '6.4'
- },
- {
- key: 'E3',
- category: 'OTHER',
- name: 'foo'
- }
- ]
- }
-];
+jest.mock('../../../../helpers/dates', () => ({
+ parseDate: jest.fn(date => `PARSED:${date}`)
+}));
-const DEFAULT_PROPS = {
- addCustomEvent: jest.fn().mockResolvedValue(undefined),
- addVersion: jest.fn().mockResolvedValue(undefined),
- analyses: ANALYSES,
- analysesLoading: false,
- branch: { isMain: true },
- changeEvent: jest.fn().mockResolvedValue(undefined),
- deleteAnalysis: jest.fn().mockResolvedValue(undefined),
- deleteEvent: jest.fn().mockResolvedValue(undefined),
- graphLoading: false,
- initializing: false,
- project: {
- key: 'foo',
- leakPeriodDate: '2017-05-16T13:50:02+0200',
- qualifier: 'TRK'
- },
- metrics: [{ id: '1', key: 'code_smells', name: 'Code Smells', type: 'INT' }],
- measuresHistory: [
- {
- metric: 'code_smells',
- history: [
- { date: parseDate('Fri Mar 04 2016 10:40:12 GMT+0100 (CET)'), value: '1749' },
- { date: parseDate('Fri Mar 04 2016 18:40:16 GMT+0100 (CET)'), value: '2286' }
- ]
- }
- ],
- query: {
- category: '',
- customMetrics: [],
- graph: DEFAULT_GRAPH,
- project: 'org.sonarsource.sonarqube:sonarqube'
- },
- updateQuery: () => {}
-};
+jest.mock('../../../../api/time-machine', () => {
+ const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ getAllTimeMachineData: jest.fn().mockResolvedValue({
+ measures: [
+ {
+ metric: 'bugs',
+ history: [{ date: '2022-01-01', value: '10' }]
+ }
+ ],
+ paging: mockPaging({ total: 1 })
+ })
+ };
+});
+
+jest.mock('../../../../api/metrics', () => {
+ const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ getAllMetrics: jest.fn().mockResolvedValue([mockMetric()])
+ };
+});
+
+jest.mock('../../../../api/projectActivity', () => {
+ const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ ...jest.requireActual('../../../../api/projectActivity'),
+ createEvent: jest.fn(),
+ changeEvent: jest.fn(),
+ getProjectActivity: jest.fn().mockResolvedValue({
+ analyses: [mockAnalysis({ key: 'foo' })],
+ paging: mockPaging({ total: 1 })
+ })
+ };
+});
it('should render correctly', () => {
- expect(shallow(<ProjectActivityApp {...DEFAULT_PROPS} />)).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should filter metric correctly', () => {
+ const wrapper = shallowRender();
+ let metrics = wrapper
+ .instance()
+ .filterMetrics(mockComponent({ qualifier: ComponentQualifier.Project }), [
+ mockMetric({ key: MetricKey.bugs }),
+ mockMetric({ key: MetricKey.security_review_rating })
+ ]);
+ expect(metrics).toHaveLength(1);
+ metrics = wrapper
+ .instance()
+ .filterMetrics(mockComponent({ qualifier: ComponentQualifier.Portfolio }), [
+ mockMetric({ key: MetricKey.bugs }),
+ mockMetric({ key: MetricKey.security_hotspots_reviewed })
+ ]);
+ expect(metrics).toHaveLength(1);
});
+
+it('should correctly create and update custom events', async () => {
+ const analysisKey = 'foo';
+ const name = 'bar';
+ const newName = 'baz';
+ const event = mockAnalysisEvent({ name });
+ (createEvent as jest.Mock).mockResolvedValueOnce({ analysis: analysisKey, ...event });
+ (changeEvent as jest.Mock).mockResolvedValueOnce({
+ analysis: analysisKey,
+ ...event,
+ name: newName
+ });
+
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ const instance = wrapper.instance();
+
+ instance.addCustomEvent(analysisKey, name);
+ expect(createEvent).toHaveBeenCalledWith(analysisKey, name, undefined);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().analyses[0].events[0]).toEqual(event);
+
+ instance.changeEvent(event.key, newName);
+ expect(changeEvent).toHaveBeenCalledWith(event.key, newName);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().analyses[0].events[0]).toEqual({ ...event, name: newName });
+});
+
+function shallowRender(props: Partial<ProjectActivityApp['props']> = {}) {
+ return shallow<ProjectActivityApp>(
+ <ProjectActivityApp
+ component={mockComponent({ breadcrumbs: [mockComponent()] })}
+ location={mockLocation()}
+ router={mockRouter()}
+ {...props}
+ />
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
-import React from 'react';
-import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext';
-import { getActivityGraph } from '../../../../components/activity-graph/utils';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import { renderApp } from '../../../../helpers/testReactTestingUtils';
-import { ComponentQualifier } from '../../../../types/component';
-import { Component } from '../../../../types/types';
-import ProjectActivityAppContainer from '../ProjectActivityAppContainer';
-
-jest.mock('../../../../api/time-machine', () => {
- const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
- return {
- getAllTimeMachineData: jest.fn().mockResolvedValue({
- measures: [
- {
- metric: 'bugs',
- history: [{ date: '2022-01-01', value: '10' }]
- }
- ],
- paging: mockPaging({ total: 1 })
- })
- };
-});
-
-jest.mock('../../../../api/metrics', () => {
- const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
- return {
- getAllMetrics: jest.fn().mockResolvedValue([mockMetric()])
- };
-});
-
-jest.mock('../../../../api/projectActivity', () => {
- const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks');
- return {
- ...jest.requireActual('../../../../api/projectActivity'),
- createEvent: jest.fn(),
- changeEvent: jest.fn(),
- getProjectActivity: jest.fn().mockResolvedValue({
- analyses: [mockAnalysis({ key: 'foo' })],
- paging: mockPaging({ total: 1 })
- })
- };
-});
-
-jest.mock('../../../../components/activity-graph/utils', () => {
- const actual = jest.requireActual('../../../../components/activity-graph/utils');
- return {
- ...actual,
- getActivityGraph: jest.fn()
- };
-});
-
-it('should render default graph', async () => {
- (getActivityGraph as jest.Mock).mockImplementation(() => {
- return {
- graph: 'issues'
- };
- });
-
- renderProjectActivityAppContainer();
-
- expect(await screen.findByText('project_activity.graphs.issues')).toBeInTheDocument();
-});
-
-it('should reload custom graph from local storage', async () => {
- (getActivityGraph as jest.Mock).mockImplementation(() => {
- return {
- graph: 'custom',
- customGraphs: ['bugs', 'code_smells']
- };
- });
-
- renderProjectActivityAppContainer();
-
- expect(await screen.findByText('project_activity.graphs.custom')).toBeInTheDocument();
-});
-
-function renderProjectActivityAppContainer(
- { component, navigateTo }: { component: Component; navigateTo?: string } = {
- component: mockComponent({
- breadcrumbs: [
- { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }
- ]
- })
- }
-) {
- return renderApp(
- 'project/activity',
- <ComponentContext.Provider
- value={{
- branchLikes: [],
- onBranchesChange: jest.fn(),
- onComponentChange: jest.fn(),
- component
- }}>
- <ProjectActivityAppContainer />
- </ComponentContext.Provider>,
- { navigateTo }
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { changeEvent, createEvent } from '../../../../api/projectActivity';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import {
- mockAnalysisEvent,
- mockLocation,
- mockMetric,
- mockRouter
-} from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { ComponentQualifier } from '../../../../types/component';
-import { MetricKey } from '../../../../types/metrics';
-import { ProjectActivityAppContainer } from '../ProjectActivityAppContainer';
-
-jest.mock('../../../../helpers/dates', () => ({
- parseDate: jest.fn(date => `PARSED:${date}`)
-}));
-
-jest.mock('../../../../api/time-machine', () => {
- const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
- return {
- getAllTimeMachineData: jest.fn().mockResolvedValue({
- measures: [
- {
- metric: 'bugs',
- history: [{ date: '2022-01-01', value: '10' }]
- }
- ],
- paging: mockPaging({ total: 1 })
- })
- };
-});
-
-jest.mock('../../../../api/metrics', () => {
- const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
- return {
- getAllMetrics: jest.fn().mockResolvedValue([mockMetric()])
- };
-});
-
-jest.mock('../../../../api/projectActivity', () => {
- const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks');
- return {
- ...jest.requireActual('../../../../api/projectActivity'),
- createEvent: jest.fn(),
- changeEvent: jest.fn(),
- getProjectActivity: jest.fn().mockResolvedValue({
- analyses: [mockAnalysis({ key: 'foo' })],
- paging: mockPaging({ total: 1 })
- })
- };
-});
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should filter metric correctly', () => {
- const wrapper = shallowRender();
- let metrics = wrapper
- .instance()
- .filterMetrics(mockComponent({ qualifier: ComponentQualifier.Project }), [
- mockMetric({ key: MetricKey.bugs }),
- mockMetric({ key: MetricKey.security_review_rating })
- ]);
- expect(metrics).toHaveLength(1);
- metrics = wrapper
- .instance()
- .filterMetrics(mockComponent({ qualifier: ComponentQualifier.Portfolio }), [
- mockMetric({ key: MetricKey.bugs }),
- mockMetric({ key: MetricKey.security_hotspots_reviewed })
- ]);
- expect(metrics).toHaveLength(1);
-});
-
-it('should correctly create and update custom events', async () => {
- const analysisKey = 'foo';
- const name = 'bar';
- const newName = 'baz';
- const event = mockAnalysisEvent({ name });
- (createEvent as jest.Mock).mockResolvedValueOnce({ analysis: analysisKey, ...event });
- (changeEvent as jest.Mock).mockResolvedValueOnce({
- analysis: analysisKey,
- ...event,
- name: newName
- });
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- const instance = wrapper.instance();
-
- instance.addCustomEvent(analysisKey, name);
- expect(createEvent).toHaveBeenCalledWith(analysisKey, name, undefined);
- await waitAndUpdate(wrapper);
- expect(wrapper.state().analyses[0].events[0]).toEqual(event);
-
- instance.changeEvent(event.key, newName);
- expect(changeEvent).toHaveBeenCalledWith(event.key, newName);
- await waitAndUpdate(wrapper);
- expect(wrapper.state().analyses[0].events[0]).toEqual({ ...event, name: newName });
-});
-
-function shallowRender(props: Partial<ProjectActivityAppContainer['props']> = {}) {
- return shallow<ProjectActivityAppContainer>(
- <ProjectActivityAppContainer
- component={mockComponent({ breadcrumbs: [mockComponent()] })}
- location={mockLocation()}
- router={mockRouter()}
- {...props}
- />
- );
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils';
+import { parseDate } from '../../../../helpers/dates';
+import ProjectActivityAppRenderer from '../ProjectActivityAppRenderer';
+
+const ANALYSES = [
+ {
+ key: 'A1',
+ date: parseDate('2016-10-27T16:33:50+0200'),
+ events: [
+ {
+ key: 'E1',
+ category: 'VERSION',
+ name: '6.5-SNAPSHOT'
+ }
+ ]
+ },
+ {
+ key: 'A2',
+ date: parseDate('2016-10-27T12:21:15+0200'),
+ events: []
+ },
+ {
+ key: 'A3',
+ date: parseDate('2016-10-26T12:17:29+0200'),
+ events: [
+ {
+ key: 'E2',
+ category: 'VERSION',
+ name: '6.4'
+ },
+ {
+ key: 'E3',
+ category: 'OTHER',
+ name: 'foo'
+ }
+ ]
+ }
+];
+
+const DEFAULT_PROPS = {
+ addCustomEvent: jest.fn().mockResolvedValue(undefined),
+ addVersion: jest.fn().mockResolvedValue(undefined),
+ analyses: ANALYSES,
+ analysesLoading: false,
+ branch: { isMain: true },
+ changeEvent: jest.fn().mockResolvedValue(undefined),
+ deleteAnalysis: jest.fn().mockResolvedValue(undefined),
+ deleteEvent: jest.fn().mockResolvedValue(undefined),
+ graphLoading: false,
+ initializing: false,
+ project: {
+ key: 'foo',
+ leakPeriodDate: '2017-05-16T13:50:02+0200',
+ qualifier: 'TRK'
+ },
+ metrics: [{ id: '1', key: 'code_smells', name: 'Code Smells', type: 'INT' }],
+ measuresHistory: [
+ {
+ metric: 'code_smells',
+ history: [
+ { date: parseDate('Fri Mar 04 2016 10:40:12 GMT+0100 (CET)'), value: '1749' },
+ { date: parseDate('Fri Mar 04 2016 18:40:16 GMT+0100 (CET)'), value: '2286' }
+ ]
+ }
+ ],
+ query: {
+ category: '',
+ customMetrics: [],
+ graph: DEFAULT_GRAPH,
+ project: 'org.sonarsource.sonarqube:sonarqube'
+ },
+ updateQuery: () => {}
+};
+
+it('should render correctly', () => {
+ expect(shallow(<ProjectActivityAppRenderer {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correctly 1`] = `
-<div
- className="page page-limited"
- id="project-activity"
->
- <Suggestions
- suggestions="project_activity"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="project_activity.page"
- />
- <A11ySkipTarget
- anchor="activity_main"
- />
- <ProjectActivityPageFilters
- category=""
- project={
- Object {
- "key": "foo",
- "leakPeriodDate": "2017-05-16T13:50:02+0200",
- "qualifier": "TRK",
- }
- }
- updateQuery={[Function]}
- />
- <div
- className="layout-page project-activity-page"
- >
- <div
- className="layout-page-side-outer project-activity-page-side-outer boxed-group"
- >
- <ProjectActivityAnalysesList
- addCustomEvent={[MockFunction]}
- addVersion={[MockFunction]}
- analyses={
- Array [
- Object {
- "date": 2016-10-27T14:33:50.000Z,
- "events": Array [
- Object {
- "category": "VERSION",
- "key": "E1",
- "name": "6.5-SNAPSHOT",
- },
- ],
- "key": "A1",
- },
- Object {
- "date": 2016-10-27T10:21:15.000Z,
- "events": Array [],
- "key": "A2",
- },
- Object {
- "date": 2016-10-26T10:17:29.000Z,
- "events": Array [
- Object {
- "category": "VERSION",
- "key": "E2",
- "name": "6.4",
- },
- Object {
- "category": "OTHER",
- "key": "E3",
- "name": "foo",
- },
- ],
- "key": "A3",
- },
- ]
- }
- analysesLoading={false}
- canAdmin={false}
- canDeleteAnalyses={false}
- changeEvent={[MockFunction]}
- deleteAnalysis={[MockFunction]}
- deleteEvent={[MockFunction]}
- initializing={false}
- leakPeriodDate={2017-05-16T11:50:02.000Z}
- project={
- Object {
- "key": "foo",
- "leakPeriodDate": "2017-05-16T13:50:02+0200",
- "qualifier": "TRK",
- }
- }
- query={
- Object {
- "category": "",
- "customMetrics": Array [],
- "graph": "issues",
- "project": "org.sonarsource.sonarqube:sonarqube",
- }
- }
- updateQuery={[Function]}
- />
- </div>
- <div
- className="project-activity-layout-page-main"
- >
- <ProjectActivityGraphs
- analyses={
- Array [
- Object {
- "date": 2016-10-27T14:33:50.000Z,
- "events": Array [
- Object {
- "category": "VERSION",
- "key": "E1",
- "name": "6.5-SNAPSHOT",
- },
- ],
- "key": "A1",
- },
+<ProjectActivityAppRenderer
+ addCustomEvent={[Function]}
+ addVersion={[Function]}
+ analyses={Array []}
+ analysesLoading={false}
+ changeEvent={[Function]}
+ deleteAnalysis={[Function]}
+ deleteEvent={[Function]}
+ graphLoading={true}
+ initializing={true}
+ measuresHistory={Array []}
+ metrics={Array []}
+ project={
+ Object {
+ "breadcrumbs": Array [
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
Object {
- "date": 2016-10-27T10:21:15.000Z,
- "events": Array [],
- "key": "A2",
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
},
- Object {
- "date": 2016-10-26T10:17:29.000Z,
- "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",
- },
- ]
- }
- metrics={
- Array [
- Object {
- "id": "1",
- "key": "code_smells",
- "name": "Code Smells",
- "type": "INT",
- },
- ]
- }
- project="foo"
- query={
- Object {
- "category": "",
- "customMetrics": Array [],
- "graph": "issues",
- "project": "org.sonarsource.sonarqube:sonarqube",
- }
- }
- updateQuery={[Function]}
- />
- </div>
- </div>
-</div>
+ ],
+ "tags": Array [],
+ },
+ ],
+ "key": "my-project",
+ "name": "MyProject",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ query={
+ Object {
+ "category": "",
+ "customMetrics": Array [],
+ "from": undefined,
+ "graph": "issues",
+ "project": "",
+ "selectedDate": undefined,
+ "to": undefined,
+ }
+ }
+ updateQuery={[Function]}
+/>
`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ProjectActivityApp
- addCustomEvent={[Function]}
- addVersion={[Function]}
- analyses={Array []}
- analysesLoading={false}
- changeEvent={[Function]}
- deleteAnalysis={[Function]}
- deleteEvent={[Function]}
- graphLoading={true}
- initializing={true}
- measuresHistory={Array []}
- metrics={Array []}
- project={
- Object {
- "breadcrumbs": Array [
- Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "TRK",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- },
- ],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "TRK",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- }
- }
- query={
- Object {
- "category": "",
- "customMetrics": Array [],
- "from": undefined,
- "graph": "issues",
- "project": "",
- "selectedDate": undefined,
- "to": undefined,
- }
- }
- updateQuery={[Function]}
-/>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="page page-limited"
+ id="project-activity"
+>
+ <Suggestions
+ suggestions="project_activity"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ prioritizeSeoTags={false}
+ title="project_activity.page"
+ />
+ <A11ySkipTarget
+ anchor="activity_main"
+ />
+ <ProjectActivityPageFilters
+ category=""
+ project={
+ Object {
+ "key": "foo",
+ "leakPeriodDate": "2017-05-16T13:50:02+0200",
+ "qualifier": "TRK",
+ }
+ }
+ updateQuery={[Function]}
+ />
+ <div
+ className="layout-page project-activity-page"
+ >
+ <div
+ className="layout-page-side-outer project-activity-page-side-outer boxed-group"
+ >
+ <ProjectActivityAnalysesList
+ addCustomEvent={[MockFunction]}
+ addVersion={[MockFunction]}
+ analyses={
+ Array [
+ Object {
+ "date": 2016-10-27T14:33:50.000Z,
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E1",
+ "name": "6.5-SNAPSHOT",
+ },
+ ],
+ "key": "A1",
+ },
+ Object {
+ "date": 2016-10-27T10:21:15.000Z,
+ "events": Array [],
+ "key": "A2",
+ },
+ Object {
+ "date": 2016-10-26T10:17:29.000Z,
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E2",
+ "name": "6.4",
+ },
+ Object {
+ "category": "OTHER",
+ "key": "E3",
+ "name": "foo",
+ },
+ ],
+ "key": "A3",
+ },
+ ]
+ }
+ analysesLoading={false}
+ canAdmin={false}
+ canDeleteAnalyses={false}
+ changeEvent={[MockFunction]}
+ deleteAnalysis={[MockFunction]}
+ deleteEvent={[MockFunction]}
+ initializing={false}
+ leakPeriodDate={2017-05-16T11:50:02.000Z}
+ project={
+ Object {
+ "key": "foo",
+ "leakPeriodDate": "2017-05-16T13:50:02+0200",
+ "qualifier": "TRK",
+ }
+ }
+ query={
+ Object {
+ "category": "",
+ "customMetrics": Array [],
+ "graph": "issues",
+ "project": "org.sonarsource.sonarqube:sonarqube",
+ }
+ }
+ updateQuery={[Function]}
+ />
+ </div>
+ <div
+ className="project-activity-layout-page-main"
+ >
+ <ProjectActivityGraphs
+ analyses={
+ Array [
+ Object {
+ "date": 2016-10-27T14:33:50.000Z,
+ "events": Array [
+ Object {
+ "category": "VERSION",
+ "key": "E1",
+ "name": "6.5-SNAPSHOT",
+ },
+ ],
+ "key": "A1",
+ },
+ Object {
+ "date": 2016-10-27T10:21:15.000Z,
+ "events": Array [],
+ "key": "A2",
+ },
+ Object {
+ "date": 2016-10-26T10:17:29.000Z,
+ "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",
+ },
+ ]
+ }
+ metrics={
+ Array [
+ Object {
+ "id": "1",
+ "key": "code_smells",
+ "name": "Code Smells",
+ "type": "INT",
+ },
+ ]
+ }
+ project="foo"
+ query={
+ Object {
+ "category": "",
+ "customMetrics": Array [],
+ "graph": "issues",
+ "project": "org.sonarsource.sonarqube:sonarqube",
+ }
+ }
+ updateQuery={[Function]}
+ />
+ </div>
+ </div>
+</div>
+`;
*/
import React from 'react';
import { Route } from 'react-router-dom';
-import ProjectActivityAppContainer from './components/ProjectActivityAppContainer';
+import ProjectActivityApp from './components/ProjectActivityApp';
-const routes = () => <Route path="project/activity" element={<ProjectActivityAppContainer />} />;
+const routes = () => <Route path="project/activity" element={<ProjectActivityApp />} />;
export default routes;