Browse Source

[NO JIRA] Rename App components to follow internal guidelines

tags/9.7.0.61563
Wouter Admiraal 1 year ago
parent
commit
425b470ddc

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/actions.ts View File

@@ -18,7 +18,7 @@
* 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) => ({

+ 382
- 77
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx View File

@@ -18,94 +18,399 @@
* 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));

+ 0
- 416
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx View File

@@ -1,416 +0,0 @@
/*
* 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));

+ 111
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx View File

@@ -0,0 +1,111 @@
/*
* 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>
);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx View File

@@ -34,7 +34,7 @@ import {
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[];

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx → server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx View File

@@ -25,7 +25,7 @@ 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';
import ProjectActivityAppContainer from '../ProjectActivityApp';

jest.mock('../../../../api/time-machine', () => {
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');

+ 106
- 72
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx View File

@@ -19,80 +19,114 @@
*/
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}
/>
);
}

+ 0
- 132
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-test.tsx View File

@@ -1,132 +0,0 @@
/*
* 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}
/>
);
}

+ 98
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppRenderer-test.tsx View File

@@ -0,0 +1,98 @@
/*
* 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();
});

+ 66
- 179
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap View File

@@ -1,185 +1,72 @@
// 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]}
/>
`;

+ 0
- 72
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppContainer-test.tsx.snap View File

@@ -1,72 +0,0 @@
// 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]}
/>
`;

+ 185
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppRenderer-test.tsx.snap View File

@@ -0,0 +1,185 @@
// 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>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/routes.tsx View File

@@ -19,8 +19,8 @@
*/
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;

Loading…
Cancel
Save