Browse Source

Fetch metrics with redux on project dashboard pages

tags/7.5
Grégoire Aubert 6 years ago
parent
commit
57e81b11a8
20 changed files with 202 additions and 185 deletions
  1. 14
    4
      server/sonar-web/src/main/js/app/components/App.tsx
  2. 7
    8
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  3. 6
    10
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  4. 8
    34
      server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx
  5. 4
    1
      server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx
  6. 1
    2
      server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx
  7. 1
    1
      server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx
  8. 2
    1
      server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap
  9. 29
    3
      server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
  10. 11
    14
      server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx
  11. 62
    66
      server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx
  12. 6
    12
      server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
  13. 28
    3
      server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
  14. 2
    9
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
  15. 5
    5
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
  16. 1
    7
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
  17. 3
    0
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap
  18. 9
    2
      server/sonar-web/src/main/js/apps/projectActivity/utils.js
  19. 1
    1
      server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts
  20. 2
    2
      server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js

+ 14
- 4
server/sonar-web/src/main/js/app/components/App.tsx View File

@@ -38,6 +38,7 @@ interface State {
canAdmin: boolean;
loading: boolean;
onSonarCloud: boolean;
organizationsEnabled: boolean;
}

class App extends React.PureComponent<Props, State> {
@@ -46,19 +47,27 @@ class App extends React.PureComponent<Props, State> {
static childContextTypes = {
branchesEnabled: PropTypes.bool.isRequired,
canAdmin: PropTypes.bool.isRequired,
onSonarCloud: PropTypes.bool
onSonarCloud: PropTypes.bool,
organizationsEnabled: PropTypes.bool
};

constructor(props: Props) {
super(props);
this.state = { branchesEnabled: false, canAdmin: false, loading: true, onSonarCloud: false };
this.state = {
branchesEnabled: false,
canAdmin: false,
loading: true,
onSonarCloud: false,
organizationsEnabled: false
};
}

getChildContext() {
return {
branchesEnabled: this.state.branchesEnabled,
canAdmin: this.state.canAdmin,
onSonarCloud: this.state.onSonarCloud
onSonarCloud: this.state.onSonarCloud,
organizationsEnabled: this.state.organizationsEnabled
};
}

@@ -93,7 +102,8 @@ class App extends React.PureComponent<Props, State> {
canAdmin: appState.canAdmin,
onSonarCloud: Boolean(
appState.settings && appState.settings['sonar.sonarcloud.enabled'] === 'true'
)
),
organizationsEnabled: appState.organizationsEnabled
});
}
return appState;

+ 7
- 8
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ComponentContainerNotFound from './ComponentContainerNotFound';
import ComponentNav from './nav/component/ComponentNav';
@@ -28,7 +29,6 @@ import { Task, getTasksForComponent } from '../../api/ce';
import { getComponentData } from '../../api/components';
import { getComponentNavigation } from '../../api/nav';
import { fetchOrganizations } from '../../store/rootActions';
import { areThereCustomOrganizations } from '../../store/rootReducer';
import { STATUSES } from '../../apps/background-tasks/constants';

interface Props {
@@ -37,7 +37,6 @@ interface Props {
location: {
query: { branch?: string; id: string };
};
organizationsEnabled?: boolean;
}

interface State {
@@ -52,6 +51,10 @@ interface State {
export class ComponentContainer extends React.PureComponent<Props, State> {
mounted: boolean;

static contextTypes = {
organizationsEnabled: PropTypes.bool
};

constructor(props: Props) {
super(props);
this.state = { branches: [], loading: true };
@@ -98,7 +101,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
([nav, data]) => {
const component = this.addQualifier({ ...nav, ...data });

if (this.props.organizationsEnabled) {
if (this.context.organizationsEnabled) {
this.props.fetchOrganizations([component.organization]);
}

@@ -197,10 +200,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
}
}

const mapStateToProps = (state: any) => ({
organizationsEnabled: areThereCustomOrganizations(state)
});

const mapDispatchToProps = { fetchOrganizations };

export default connect<any, any, any>(mapStateToProps, mapDispatchToProps)(ComponentContainer);
export default connect<any, any, any>(null, mapDispatchToProps)(ComponentContainer);

+ 6
- 10
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx View File

@@ -132,12 +132,10 @@ it('loads organization', async () => {

const fetchOrganizations = jest.fn();
mount(
<ComponentContainer
fetchOrganizations={fetchOrganizations}
location={{ query: { id: 'foo' } }}
organizationsEnabled={true}>
<ComponentContainer fetchOrganizations={fetchOrganizations} location={{ query: { id: 'foo' } }}>
<Inner />
</ComponentContainer>
</ComponentContainer>,
{ context: { organizationsEnabled: true } }
);

await new Promise(setImmediate);
@@ -150,12 +148,10 @@ it('fetches status', async () => {
);

mount(
<ComponentContainer
fetchOrganizations={jest.fn()}
location={{ query: { id: 'foo' } }}
organizationsEnabled={true}>
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}>
<Inner />
</ComponentContainer>
</ComponentContainer>,
{ context: { organizationsEnabled: true } }
);

await new Promise(setImmediate);

+ 8
- 34
server/sonar-web/src/main/js/apps/overview/badges/BadgeParams.tsx View File

@@ -18,43 +18,30 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { connect } from 'react-redux';
import Select from '../../../components/controls/Select';
import { fetchWebApi } from '../../../api/web-api';
import { Metric } from '../../../app/types';
import { BadgeColors, BadgeType, BadgeOptions } from './utils';
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { fetchMetrics } from '../../../store/rootActions';
import { getMetrics } from '../../../store/rootReducer';

interface StateToProps {
metrics: { [key: string]: Metric };
}

interface DispatchToProps {
fetchMetrics: () => void;
}
import { Metric } from '../../../app/types';

interface OwnProps {
interface Props {
className?: string;
metrics: { [key: string]: Metric };
options: BadgeOptions;
type: BadgeType;
updateOptions: (options: Partial<BadgeOptions>) => void;
}

type Props = StateToProps & DispatchToProps & OwnProps;

interface State {
badgeMetrics: string[];
}

export class BadgeParams extends React.PureComponent<Props> {
export default class BadgeParams extends React.PureComponent<Props> {
mounted: boolean;
state: State = { badgeMetrics: [] };

componentDidMount() {
this.mounted = true;
this.props.fetchMetrics();
this.fetchBadgeMetrics();
}

@@ -84,16 +71,14 @@ export class BadgeParams extends React.PureComponent<Props> {
value: color
}));

getMetricOptions = () => {
const { metrics } = this.props;
return this.state.badgeMetrics.map(key => {
const metric = metrics[key];
getMetricOptions = () =>
this.state.badgeMetrics.map(key => {
const metric = this.props.metrics[key];
return {
value: key,
label: metric && getLocalizedMetricName(metric)
label: metric ? getLocalizedMetricName(metric) : key
};
});
};

handleColorChange = ({ value }: { value: BadgeColors }) =>
this.props.updateOptions({ color: value });
@@ -143,14 +128,3 @@ export class BadgeParams extends React.PureComponent<Props> {
}
}
}

const mapDispatchToProps: DispatchToProps = { fetchMetrics };

const mapStateToProps = (state: any): StateToProps => ({
metrics: getMetrics(state)
});

export default connect<StateToProps, DispatchToProps, OwnProps>(
mapStateToProps,
mapDispatchToProps
)(BadgeParams);

+ 4
- 1
server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx View File

@@ -22,12 +22,14 @@ import Modal from '../../../components/controls/Modal';
import BadgeButton from './BadgeButton';
import BadgeSnippet from './BadgeSnippet';
import BadgeParams from './BadgeParams';
import { getBadgeUrl, BadgeType, BadgeOptions } from './utils';
import { BadgeType, BadgeOptions, getBadgeUrl } from './utils';
import { Metric } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
import './styles.css';

interface Props {
branch?: string;
metrics: { [key: string]: Metric };
project: string;
}

@@ -90,6 +92,7 @@ export default class BadgesModal extends React.PureComponent<Props, State> {
</p>
<BadgeParams
className="big-spacer-bottom"
metrics={this.props.metrics}
options={badgeOptions}
type={selectedType}
updateOptions={this.handleUpdateOptions}

+ 1
- 2
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgeParams-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { BadgeParams } from '../BadgeParams';
import BadgeParams from '../BadgeParams';
import { BadgeType } from '../utils';
import { Metric } from '../../../../app/types';

@@ -62,7 +62,6 @@ it('should display measure badge params', () => {
function getWrapper(props = {}) {
return shallow(
<BadgeParams
fetchMetrics={jest.fn()}
metrics={METRICS}
options={{ color: 'white', metric: 'alert_status' }}
type={BadgeType.marketing}

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx View File

@@ -27,7 +27,7 @@ jest.mock('../../../../helpers/urls', () => ({
}));

it('should display the modal after click', () => {
const wrapper = shallow(<BadgesModal branch="branch-6.6" project="foo" />);
const wrapper = shallow(<BadgesModal branch="branch-6.6" metrics={{}} project="foo" />);
expect(wrapper).toMatchSnapshot();
click(wrapper.find('button'));
expect(wrapper.find('Modal')).toMatchSnapshot();

+ 2
- 1
server/sonar-web/src/main/js/apps/overview/badges/__tests__/__snapshots__/BadgesModal-test.tsx.snap View File

@@ -61,8 +61,9 @@ exports[`should display the modal after click 2`] = `
>
overview.badges.measure.description
</p>
<Connect(BadgeParams)
<BadgeParams
className="big-spacer-bottom"
metrics={Object {}}
options={
Object {
"color": "white",

+ 29
- 3
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx View File

@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import { uniq } from 'lodash';
import { connect } from 'react-redux';
import QualityGate from '../qualityGate/QualityGate';
import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate';
import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities';
@@ -36,15 +37,27 @@ import { getCustomGraph, getGraph } from '../../../helpers/storage';
import { METRICS, HISTORY_METRICS_LIST } from '../utils';
import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils';
import { getBranchName } from '../../../helpers/branches';
import { Branch, Component } from '../../../app/types';
import { fetchMetrics } from '../../../store/rootActions';
import { getMetrics } from '../../../store/rootReducer';
import { Branch, Component, Metric } from '../../../app/types';
import '../styles.css';

interface Props {
interface OwnProps {
branch?: Branch;
component: Component;
onComponentChange: (changes: {}) => void;
}

interface StateToProps {
metrics: { [key: string]: Metric };
}

interface DispatchToProps {
fetchMetrics: () => void;
}

type Props = StateToProps & DispatchToProps & OwnProps;

interface State {
history?: History;
historyStartDate?: Date;
@@ -53,12 +66,13 @@ interface State {
periods?: Period[];
}

export default class OverviewApp extends React.PureComponent<Props, State> {
export class OverviewApp extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: true, measures: [] };

componentDidMount() {
this.mounted = true;
this.props.fetchMetrics();
this.loadMeasures().then(this.loadHistory, () => {});
}

@@ -183,6 +197,7 @@ export default class OverviewApp extends React.PureComponent<Props, State> {
component={component}
history={history}
measures={measures}
metrics={this.props.metrics}
onComponentChange={this.props.onComponentChange}
/>
</div>
@@ -191,3 +206,14 @@ export default class OverviewApp extends React.PureComponent<Props, State> {
);
}
}

const mapDispatchToProps: DispatchToProps = { fetchMetrics };

const mapStateToProps = (state: any): StateToProps => ({
metrics: getMetrics(state)
});

export default connect<StateToProps, DispatchToProps, OwnProps>(
mapStateToProps,
mapDispatchToProps
)(OverviewApp);

+ 11
- 14
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx View File

@@ -20,7 +20,6 @@
import * as React from 'react';
import { Link } from 'react-router';
import Analysis from './Analysis';
import { getAllMetrics } from '../../../api/metrics';
import { getProjectActivity, Analysis as IAnalysis } from '../../../api/projectActivity';
import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
import { translate } from '../../../helpers/l10n';
@@ -31,20 +30,20 @@ interface Props {
branch?: string;
component: Component;
history?: History;
metrics: { [key: string]: Metric };
qualifier: string;
}

interface State {
analyses: IAnalysis[];
loading: boolean;
metrics: Metric[];
}

const PAGE_SIZE = 3;

export default class AnalysesList extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { analyses: [], loading: true, metrics: [] };
state: State = { analyses: [], loading: true };

componentDidMount() {
this.mounted = true;
@@ -75,17 +74,15 @@ export default class AnalysesList extends React.PureComponent<Props, State> {

fetchData = () => {
this.setState({ loading: true });
Promise.all([
getProjectActivity({
branch: this.props.branch,
project: this.getTopLevelComponent(),
ps: PAGE_SIZE
}),
getAllMetrics()
]).then(
([{ analyses }, metrics]) => {

getProjectActivity({
branch: this.props.branch,
project: this.getTopLevelComponent(),
ps: PAGE_SIZE
}).then(
({ analyses }) => {
if (this.mounted) {
this.setState({ analyses, metrics, loading: false });
this.setState({ analyses, loading: false });
}
},
() => {
@@ -125,7 +122,7 @@ export default class AnalysesList extends React.PureComponent<Props, State> {
branch={this.props.branch}
history={this.props.history}
project={this.props.component.key}
metrics={this.state.metrics}
metrics={this.props.metrics}
/>

{this.renderList(analyses)}

+ 62
- 66
server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { connect } from 'react-redux';
import * as PropTypes from 'prop-types';
import MetaKey from './MetaKey';
import MetaOrganizationKey from './MetaOrganizationKey';
import MetaLinks from './MetaLinks';
@@ -28,90 +28,86 @@ import AnalysesList from '../events/AnalysesList';
import MetaSize from './MetaSize';
import MetaTags from './MetaTags';
import BadgesModal from '../badges/BadgesModal';
import { areThereCustomOrganizations, getGlobalSettingValue } from '../../../store/rootReducer';
import { Visibility, Component } from '../../../app/types';
import { Visibility, Component, Metric } from '../../../app/types';
import { History } from '../../../api/time-machine';
import { MeasureEnhanced } from '../../../helpers/measures';

interface OwnProps {
interface Props {
branch?: string;
component: Component;
history?: History;
measures: MeasureEnhanced[];
metrics: { [key: string]: Metric };
onComponentChange: (changes: {}) => void;
}

interface StateToProps {
areThereCustomOrganizations: boolean;
onSonarCloud: boolean;
}

export function Meta(props: OwnProps & StateToProps) {
const { branch, component, areThereCustomOrganizations } = props;
const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;

const isProject = qualifier === 'TRK';
const isPrivate = visibility === Visibility.Private;
export default class Meta extends React.PureComponent<Props> {
static contextTypes = {
onSonarCloud: PropTypes.bool,
organizationsEnabled: PropTypes.bool
};

const hasDescription = !!description;
const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
const hasQualityGate = !!qualityGate;
render() {
const { onSonarCloud, organizationsEnabled } = this.context;
const { branch, component, metrics } = this.props;
const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;

const shouldShowQualityProfiles = isProject && hasQualityProfiles;
const shouldShowQualityGate = isProject && hasQualityGate;
const hasOrganization = component.organization != null && areThereCustomOrganizations;
const isProject = qualifier === 'TRK';
const isPrivate = visibility === Visibility.Private;

return (
<div className="overview-meta">
{hasDescription && (
<div className="overview-meta-card overview-meta-description">{description}</div>
)}
const hasDescription = !!description;
const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
const hasQualityGate = !!qualityGate;

<MetaSize branch={branch} component={component} measures={props.measures} />
const shouldShowQualityProfiles = isProject && hasQualityProfiles;
const shouldShowQualityGate = isProject && hasQualityGate;
const hasOrganization = component.organization != null && organizationsEnabled;

{isProject && <MetaTags component={component} onComponentChange={props.onComponentChange} />}
return (
<div className="overview-meta">
{hasDescription && (
<div className="overview-meta-card overview-meta-description">{description}</div>
)}

<AnalysesList
branch={branch}
component={component}
qualifier={component.qualifier}
history={props.history}
/>
<MetaSize branch={branch} component={component} measures={this.props.measures} />

{shouldShowQualityGate && (
<MetaQualityGate
gate={qualityGate}
organization={hasOrganization && component.organization}
/>
)}
{isProject && (
<MetaTags component={component} onComponentChange={this.props.onComponentChange} />
)}

{shouldShowQualityProfiles && (
<MetaQualityProfiles
<AnalysesList
branch={branch}
component={component}
customOrganizations={areThereCustomOrganizations}
profiles={qualityProfiles}
history={this.props.history}
metrics={metrics}
qualifier={component.qualifier}
/>
)}

{isProject && <MetaLinks component={component} />}

<MetaKey component={component} />

{hasOrganization && <MetaOrganizationKey component={component} />}

{props.onSonarCloud &&
isProject &&
!isPrivate && <BadgesModal branch={branch} project={component.key} />}
</div>
);
{shouldShowQualityGate && (
<MetaQualityGate
gate={qualityGate}
organization={hasOrganization && component.organization}
/>
)}

{shouldShowQualityProfiles && (
<MetaQualityProfiles
component={component}
customOrganizations={organizationsEnabled}
profiles={qualityProfiles}
/>
)}

{isProject && <MetaLinks component={component} />}

<MetaKey component={component} />

{hasOrganization && <MetaOrganizationKey component={component} />}

{onSonarCloud &&
isProject &&
!isPrivate && <BadgesModal branch={branch} metrics={metrics} project={component.key} />}
</div>
);
}
}

const mapStateToProps = (state: any): StateToProps => {
const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');
return {
areThereCustomOrganizations: areThereCustomOrganizations(state),
onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true')
};
};

export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(Meta);

+ 6
- 12
server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx View File

@@ -20,8 +20,7 @@
import * as React from 'react';
import { getDisplayedHistoryMetrics, DEFAULT_GRAPH } from '../../projectActivity/utils';
import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
import { getAllMetrics } from '../../../api/metrics';
import { getAllTimeMachineData } from '../../../api/time-machine';
import { getAllTimeMachineData, History } from '../../../api/time-machine';
import { Metric } from '../../../app/types';
import { parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
@@ -29,18 +28,14 @@ import { getCustomGraph, getGraph } from '../../../helpers/storage';

const AnyPreviewGraph = PreviewGraph as any;

interface History {
[metric: string]: Array<{ date: Date; value: string }>;
}

interface Props {
component: string;
metrics: { [key: string]: Metric };
}

interface State {
history?: History;
loading: boolean;
metrics?: Metric[];
}

export default class Activity extends React.PureComponent<Props> {
@@ -71,8 +66,8 @@ export default class Activity extends React.PureComponent<Props> {
}

this.setState({ loading: true });
return Promise.all([getAllTimeMachineData(component, graphMetrics), getAllMetrics()]).then(
([timeMachine, metrics]) => {
return getAllTimeMachineData(component, graphMetrics).then(
timeMachine => {
if (this.mounted) {
const history: History = {};
timeMachine.measures.forEach(measure => {
@@ -82,7 +77,7 @@ export default class Activity extends React.PureComponent<Props> {
}));
history[measure.metric] = measureHistory;
});
this.setState({ history, loading: false, metrics });
this.setState({ history, loading: false });
}
},
() => {
@@ -103,11 +98,10 @@ export default class Activity extends React.PureComponent<Props> {
{this.state.loading ? (
<i className="spinner" />
) : (
this.state.metrics !== undefined &&
this.state.history !== undefined && (
<AnyPreviewGraph
history={this.state.history}
metrics={this.state.metrics}
metrics={this.props.metrics}
project={this.props.component}
renderWhenEmpty={this.renderWhenEmpty}
/>

+ 28
- 3
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { connect } from 'react-redux';
import Summary from './Summary';
import Report from './Report';
import WorstProjects from './WorstProjects';
@@ -31,12 +32,25 @@ import { PORTFOLIO_METRICS, SUB_COMPONENTS_METRICS, convertMeasures } from '../u
import { getMeasures } from '../../../api/measures';
import { getChildren } from '../../../api/components';
import { translate } from '../../../helpers/l10n';
import { fetchMetrics } from '../../../store/rootActions';
import { getMetrics } from '../../../store/rootReducer';
import { Metric } from '../../../app/types';
import '../styles.css';

interface Props {
interface OwnProps {
component: { key: string; name: string };
}

interface StateToProps {
metrics: { [key: string]: Metric };
}

interface DispatchToProps {
fetchMetrics: () => void;
}

type Props = StateToProps & DispatchToProps & OwnProps;

interface State {
loading: boolean;
measures?: { [key: string]: string | undefined };
@@ -44,12 +58,13 @@ interface State {
totalSubComponents?: number;
}

export default class App extends React.PureComponent<Props, State> {
export class App extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
this.props.fetchMetrics();
this.fetchData();
}

@@ -171,7 +186,7 @@ export default class App extends React.PureComponent<Props, State> {
<aside className="page-sidebar-fixed">
{!this.isEmpty() &&
!this.isNotComputed() && <Summary component={component} measures={measures!} />}
<Activity component={component.key} />
<Activity component={component.key} metrics={this.props.metrics} />
<Report component={component} />
</aside>
</div>
@@ -179,3 +194,13 @@ export default class App extends React.PureComponent<Props, State> {
);
}
}

const mapDispatchToProps: DispatchToProps = { fetchMetrics };

const mapStateToProps = (state: any): StateToProps => ({
metrics: getMetrics(state)
});

export default connect<StateToProps, DispatchToProps, Props>(mapStateToProps, mapDispatchToProps)(
App
);

+ 2
- 9
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx View File

@@ -23,10 +23,6 @@ jest.mock('../../../../helpers/storage', () => ({
getGraph: () => 'custom'
}));

jest.mock('../../../../api/metrics', () => ({
getAllMetrics: jest.fn(() => Promise.resolve([]))
}));

jest.mock('../../../../api/time-machine', () => ({
getAllTimeMachineData: jest.fn(() =>
Promise.resolve({
@@ -47,17 +43,15 @@ import * as React from 'react';
import { mount, shallow } from 'enzyme';
import Activity from '../Activity';

const getAllMetrics = require('../../../../api/metrics').getAllMetrics as jest.Mock<any>;
const getAllTimeMachineData = require('../../../../api/time-machine')
.getAllTimeMachineData as jest.Mock<any>;

beforeEach(() => {
getAllMetrics.mockClear();
getAllTimeMachineData.mockClear();
});

it('renders', () => {
const wrapper = shallow(<Activity component="foo" />);
const wrapper = shallow(<Activity component="foo" metrics={{}} />);
wrapper.setState({
history: {
coverage: [
@@ -72,7 +66,6 @@ it('renders', () => {
});

it('fetches history', () => {
mount(<Activity component="foo" />);
expect(getAllMetrics).toBeCalled();
mount(<Activity component="foo" metrics={{}} />);
expect(getAllTimeMachineData).toBeCalledWith('foo', ['coverage']);
});

+ 5
- 5
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx View File

@@ -43,7 +43,7 @@ jest.mock('../Report', () => ({

import * as React from 'react';
import { shallow, mount } from 'enzyme';
import App from '../App';
import { App } from '../App';

const getMeasures = require('../../../../api/measures').getMeasures as jest.Mock<any>;
const getChildren = require('../../../../api/components').getChildren as jest.Mock<any>;
@@ -51,7 +51,7 @@ const getChildren = require('../../../../api/components').getChildren as jest.Mo
const component = { key: 'foo', name: 'Foo' };

it('renders', () => {
const wrapper = shallow(<App component={component} />);
const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
wrapper.setState({
loading: false,
measures: { ncloc: '173', reliability_rating: '1' },
@@ -62,13 +62,13 @@ it('renders', () => {
});

it('renders when portfolio is empty', () => {
const wrapper = shallow(<App component={component} />);
const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
wrapper.setState({ loading: false, measures: { reliability_rating: '1' } });
expect(wrapper).toMatchSnapshot();
});

it('renders when portfolio is not computed', () => {
const wrapper = shallow(<App component={component} />);
const wrapper = shallow(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
wrapper.setState({ loading: false, measures: { ncloc: '173' } });
expect(wrapper).toMatchSnapshot();
});
@@ -76,7 +76,7 @@ it('renders when portfolio is not computed', () => {
it('fetches measures and children components', () => {
getMeasures.mockClear();
getChildren.mockClear();
mount(<App component={component} />);
mount(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
expect(getMeasures).toBeCalledWith('foo', [
'projects',
'ncloc',

+ 1
- 7
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap View File

@@ -22,13 +22,7 @@ exports[`renders 1`] = `
],
}
}
metrics={
Array [
Object {
"key": "coverage",
},
]
}
metrics={Object {}}
project="foo"
renderWhenEmpty={[Function]}
/>

+ 3
- 0
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -77,6 +77,7 @@ exports[`renders 1`] = `
/>
<Activity
component="foo"
metrics={Object {}}
/>
<Report
component={
@@ -117,6 +118,7 @@ exports[`renders when portfolio is empty 1`] = `
>
<Activity
component="foo"
metrics={Object {}}
/>
<Report
component={
@@ -154,6 +156,7 @@ exports[`renders when portfolio is not computed 1`] = `
>
<Activity
component="foo"
metrics={Object {}}
/>
<Report
component={

+ 9
- 2
server/sonar-web/src/main/js/apps/projectActivity/utils.js View File

@@ -98,10 +98,17 @@ export const generateCoveredLinesMetric = (
};
};

function findMetric(key /*: string */, metrics /*: Array<Metric> | { [string]: Metric } */) {
if (Array.isArray(metrics)) {
return metrics.find(metric => metric.key === key);
}
return metrics[key];
}

export function generateSeries(
measuresHistory /*: Array<MeasureHistory> */,
graph /*: string */,
metrics /*: Array<Metric> */,
metrics /*: Array<Metric> | { [string]: Metric } */,
displayedMetrics /*: Array<string> */
) /*: Array<Serie> */ {
if (displayedMetrics.length <= 0) {
@@ -114,7 +121,7 @@ export function generateSeries(
if (measure.metric === 'uncovered_lines' && !isCustomGraph(graph)) {
return generateCoveredLinesMetric(measure, measuresHistory);
}
const metric = metrics.find(metric => metric.key === measure.metric);
const metric = findMetric(measure.metric, metrics);
return {
data: measure.history.map(analysis => ({
x: analysis.date,

+ 1
- 1
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts View File

@@ -24,7 +24,7 @@ import { Metric } from '../../app/types';
interface Props {
branch?: string;
history?: History;
metrics: Metric[];
metrics: { [key: string]: Metric };
project: string;
renderWhenEmpty?: () => void;
}

+ 2
- 2
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js View File

@@ -41,7 +41,7 @@ import { formatMeasure, getShortType } from '../../helpers/measures';
type Props = {
branch?: string,
history: ?History,
metrics: Array<Metric>,
metrics: { [string]: Metric },
project: string,
renderWhenEmpty?: () => void
};
@@ -121,7 +121,7 @@ export default class PreviewGraph extends React.PureComponent {
history /*: ?History */,
graph /*: string */,
customMetrics /*: Array<string> */,
metrics /*: Array<Metric> */
metrics /*: { [string]: Metric } */
) => {
const myHistory = history;
if (!myHistory) {

Loading…
Cancel
Save