Browse Source

SONAR-11165 Migrate rest of component measures page to TS

tags/7.5
Grégoire Aubert 5 years ago
parent
commit
0ba8c56c9a
58 changed files with 601 additions and 652 deletions
  1. 5
    5
      server/sonar-web/src/main/js/api/components.ts
  2. 5
    5
      server/sonar-web/src/main/js/api/measures.ts
  3. 0
    6
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  4. 18
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  5. 0
    1
      server/sonar-web/src/main/js/app/styles/components/modals.css
  6. 30
    12
      server/sonar-web/src/main/js/app/types.ts
  7. 59
    61
      server/sonar-web/src/main/js/apps/component-measures/components/App.tsx
  8. 53
    25
      server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.tsx
  9. 8
    9
      server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.tsx
  10. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx
  11. 3
    5
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
  12. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.tsx
  13. 14
    17
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
  14. 38
    34
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
  15. 35
    41
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
  16. 2
    3
      server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.tsx
  17. 11
    12
      server/sonar-web/src/main/js/apps/component-measures/components/PageActions.tsx
  18. 13
    7
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx
  19. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.tsx
  20. 1
    2
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx
  21. 9
    25
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx
  22. 9
    3
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.tsx
  23. 35
    3
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap
  24. 0
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.tsx.snap
  25. 3
    48
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap
  26. 0
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.tsx.snap
  27. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts
  28. 6
    1
      server/sonar-web/src/main/js/apps/component-measures/config/domains.ts
  29. 36
    57
      server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx
  30. 7
    2
      server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.tsx
  31. 36
    41
      server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx
  32. 13
    15
      server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx
  33. 9
    12
      server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx
  34. 4
    4
      server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.tsx
  35. 19
    21
      server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx
  36. 7
    4
      server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx
  37. 9
    13
      server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx
  38. 23
    26
      server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
  39. 5
    4
      server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.tsx
  40. 3
    2
      server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.tsx
  41. 6
    3
      server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx
  42. 16
    0
      server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.tsx.snap
  43. 0
    0
      server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap
  44. 3
    0
      server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
  45. 0
    58
      server/sonar-web/src/main/js/apps/component-measures/types.js
  46. 3
    6
      server/sonar-web/src/main/js/apps/component-measures/utils.ts
  47. 2
    1
      server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx
  48. 2
    2
      server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
  49. 1
    1
      server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.tsx
  50. 2
    2
      server/sonar-web/src/main/js/apps/overview/main/enhance.tsx
  51. 1
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
  52. 0
    1
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
  53. 16
    12
      server/sonar-web/src/main/js/components/charts/BubbleChart.tsx
  54. 1
    1
      server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx
  55. 2
    2
      server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap
  56. 2
    1
      server/sonar-web/src/main/js/components/measure/utils.ts
  57. 6
    10
      server/sonar-web/src/main/js/helpers/path.ts
  58. 3
    19
      server/sonar-web/src/main/js/helpers/periods.ts

+ 5
- 5
server/sonar-web/src/main/js/api/components.ts View File

@@ -114,14 +114,14 @@ export function getComponentTree(
metricKeys: metrics.join(','),
strategy
});
return getJSON(url, data);
return getJSON(url, data).catch(throwGlobalError);
}

export function getChildren(
componentKey: string,
metrics: string[] = [],
additional: RequestData = {}
): Promise<any> {
) {
return getComponentTree('children', componentKey, metrics, additional);
}

@@ -129,14 +129,14 @@ export function getComponentLeaves(
componentKey: string,
metrics: string[] = [],
additional: RequestData = {}
): Promise<any> {
) {
return getComponentTree('leaves', componentKey, metrics, additional);
}

export function getComponent(
data: { componentKey: string; metricKeys: string } & BranchParameters
): Promise<any> {
return getJSON('/api/measures/component', data).then(r => r.component);
return getJSON('/api/measures/component', data).then(r => r.component, throwGlobalError);
}

export interface TreeComponent extends LightComponent {
@@ -165,7 +165,7 @@ export function getTree(data: {
}

export function getComponentShow(data: { component: string } & BranchParameters): Promise<any> {
return getJSON('/api/components/show', data);
return getJSON('/api/components/show', data).catch(throwGlobalError);
}

export function getParents(component: string): Promise<any> {

+ 5
- 5
server/sonar-web/src/main/js/api/measures.ts View File

@@ -20,14 +20,14 @@
import { getJSON, RequestData, postJSON, post } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
import {
Metric,
CustomMeasure,
Paging,
BranchParameters,
Measure,
MeasurePeriod
Metric,
Paging,
Period,
PeriodMeasure
} from '../app/types';
import { Period } from '../helpers/periods';

export function getMeasures(
data: { componentKey: string; metricKeys: string } & BranchParameters
@@ -55,7 +55,7 @@ export function getMeasuresAndMeta(
interface MeasuresForProjects {
component: string;
metric: string;
periods?: MeasurePeriod[];
periods?: PeriodMeasure[];
value?: string;
}


+ 0
- 6
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

@@ -152,12 +152,6 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
}

renderComponentMeasuresLink() {
const { branchLike } = this.props;

if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) {
return null;
}

return (
<li>
<Link

+ 18
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap View File

@@ -1097,6 +1097,24 @@ exports[`should work for short-living branches 1`] = `
issues.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/component_measures",
"query": Object {
"branch": "feature",
"id": "foo",
},
}
}
>
layout.measures
</Link>
</li>
<li>
<Link
activeClassName="active"

+ 0
- 1
server/sonar-web/src/main/js/app/styles/components/modals.css View File

@@ -275,7 +275,6 @@
}

.modal-foot {
line-height: var(--controlHeight);
padding: 10px;
border-top: 1px solid #ccc;
background-color: var(--gray94);

+ 30
- 12
server/sonar-web/src/main/js/app/types.ts View File

@@ -107,12 +107,14 @@ export interface ComponentQualityProfile {
}

interface ComponentMeasureIntern {
branch?: string;
isFavorite?: boolean;
isRecentlyBrowsed?: boolean;
key: string;
match?: string;
name: string;
organization?: string;
path?: string;
project?: string;
qualifier: string;
refKey?: string;
@@ -376,18 +378,6 @@ export interface MainBranch extends Branch {
status?: { qualityGateStatus: string };
}

export interface MeasurePeriod {
bestValue?: boolean;
index: number;
value: string;
}

interface MeasureIntern {
bestValue?: boolean;
periods?: MeasurePeriod[];
value?: string;
}

export interface Measure extends MeasureIntern {
metric: string;
}
@@ -397,6 +387,12 @@ export interface MeasureEnhanced extends MeasureIntern {
leak?: string;
}

interface MeasureIntern {
bestValue?: boolean;
periods?: PeriodMeasure[];
value?: string;
}

export interface Metric {
bestValue?: string;
custom?: boolean;
@@ -478,6 +474,28 @@ export interface Paging {
total: number;
}

export interface Period {
date: string;
index: number;
mode: PeriodMode;
modeParam?: string;
parameter?: string;
}

export interface PeriodMeasure {
bestValue?: boolean;
index: number;
value: string;
}

export enum PeriodMode {
Days = 'days',
Date = 'date',
Version = 'version',
PreviousAnalysis = 'previous_analysis',
PreviousVersion = 'previous_version'
}

export interface PermissionTemplate {
defaultFor: string[];
id: string;

server/sonar-web/src/main/js/apps/component-measures/components/App.js → server/sonar-web/src/main/js/apps/component-measures/components/App.tsx View File

@@ -17,15 +17,15 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import * as key from 'keymaster';
import { InjectedRouter } from 'react-router';
import Helmet from 'react-helmet';
import key from 'keymaster';
import MeasureContentContainer from './MeasureContentContainer';
import MeasureOverviewContainer from './MeasureOverviewContainer';
import Sidebar from '../sidebar/Sidebar';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery } from '../utils';
import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery, Query } from '../utils';
import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import {
@@ -34,67 +34,64 @@ import {
translate
} from '../../../helpers/l10n';
import { getDisplayMetrics } from '../../../helpers/measures';
/*:: import type { Component, Query, Period } from '../types'; */
/*:: import type { RawQuery } from '../../../helpers/query'; */
/*:: import type { Metric } from '../../../app/flow-types'; */
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
import { RawQuery } from '../../../helpers/query';
import {
BranchLike,
ComponentMeasure,
MeasureEnhanced,
Metric,
CurrentUser,
Period
} from '../../../app/types';
import '../../../components/search-navigator.css';
import '../style.css';

/*:: type Props = {|
branchLike?: { id?: string; name: string },
component: Component,
currentUser: { isLoggedIn: boolean },
location: { pathname: string, query: RawQuery },
interface Props {
branchLike?: BranchLike;
component: ComponentMeasure;
currentUser: CurrentUser;
location: { pathname: string; query: RawQuery };
fetchMeasures: (
component: string,
metricsKey: Array<string>,
branchLike?: { id?: string; name: string }
) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }>,
fetchMetrics: () => void,
metrics: { [string]: Metric },
metricsKey: Array<string>,
router: {
push: ({ pathname: string, query?: RawQuery }) => void
}
|}; */
metricsKey: string[],
branchLike?: BranchLike
) => Promise<{ component: ComponentMeasure; measures: MeasureEnhanced[]; leakPeriod?: Period }>;
fetchMetrics: () => void;
metrics: { [metric: string]: Metric };
metricsKey: string[];
router: InjectedRouter;
}

/*:: type State = {|
loading: boolean,
measures: Array<MeasureEnhanced>,
leakPeriod: ?Period
|}; */
interface State {
loading: boolean;
measures: MeasureEnhanced[];
leakPeriod?: Period;
}

export default class App extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
/*:: state: State; */
export default class App extends React.PureComponent<Props, State> {
mounted = false;

constructor(props /*: Props */) {
constructor(props: Props) {
super(props);
this.state = {
loading: true,
measures: [],
leakPeriod: null
};
this.state = { loading: true, measures: [] };
}

componentDidMount() {
this.mounted = true;
// $FlowFixMe
document.body.classList.add('white-page');
// $FlowFixMe
document.documentElement.classList.add('white-page');
this.props.fetchMetrics();
this.fetchMeasures(this.props);
key.setScope('measures-files');
const footer = document.getElementById('footer');
if (footer) {
footer.classList.add('page-footer-with-sidebar');
}

key.setScope('measures-files');
this.props.fetchMetrics();
this.fetchMeasures(this.props);
}

componentWillReceiveProps(nextProps /*: Props */) {
componentWillReceiveProps(nextProps: Props) {
if (
!isSameBranchLike(nextProps.branchLike, this.props.branchLike) ||
nextProps.component.key !== this.props.component.key ||
@@ -106,27 +103,31 @@ export default class App extends React.PureComponent {

componentWillUnmount() {
this.mounted = false;
// $FlowFixMe
document.body.classList.remove('white-page');
// $FlowFixMe
document.documentElement.classList.remove('white-page');
key.deleteScope('measures-files');
const footer = document.getElementById('footer');
if (footer) {
footer.classList.remove('page-footer-with-sidebar');
}

key.deleteScope('measures-files');
}

fetchMeasures = ({ branchLike, component, fetchMeasures, metrics } /*: Props */) => {
fetchMeasures = ({ branchLike, component, fetchMeasures, metrics }: Props) => {
this.setState({ loading: true });
const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key);

fetchMeasures(component.key, filteredKeys, branchLike).then(
({ measures, leakPeriod }) => {
if (this.mounted) {
this.setState({
loading: false,
leakPeriod,
measures: measures.filter(measure => measure.value != null || measure.leak != null)
measures: measures.filter(
measure => measure.value !== undefined || measure.leak !== undefined
)
});
}
},
@@ -138,7 +139,7 @@ export default class App extends React.PureComponent {
);
};

updateQuery = (newQuery /*: Query */) => {
updateQuery = (newQuery: Partial<Query>) => {
const query = serializeQuery({
...parseQuery(this.props.location.query),
...newQuery
@@ -153,19 +154,16 @@ export default class App extends React.PureComponent {
});
};

getHelmetTitle = (
metric /*: Metric */,
query /*: {metric: string, selected: string, view: string }*/
) => {
if (metric == null && hasBubbleChart(query.metric)) {
return isProjectOverview(query.metric)
getHelmetTitle = (metric?: Metric) => {
if (metric && hasBubbleChart(metric.key)) {
return isProjectOverview(metric.key)
? translate('component_measures.overview.project_overview.facet')
: translateWithParameters(
'component_measures.domain_x_overview',
getLocalizedMetricDomain(query.metric)
getLocalizedMetricDomain(metric.key)
);
}
return metric != null ? metric.name : translate('layout.measures');
return metric ? metric.name : translate('layout.measures');
};

render() {
@@ -180,7 +178,7 @@ export default class App extends React.PureComponent {
return (
<div className="layout-page" id="component-measures">
<Suggestions suggestions="component_measures" />
<Helmet title={this.getHelmetTitle(metric, query)} />
<Helmet title={this.getHelmetTitle(metric)} />

<ScreenPositionHelper className="layout-page-side-outer">
{({ top }) => (
@@ -198,7 +196,7 @@ export default class App extends React.PureComponent {
)}
</ScreenPositionHelper>

{metric != null && (
{metric && (
<MeasureContentContainer
branchLike={branchLike}
className="layout-page-main"
@@ -214,7 +212,7 @@ export default class App extends React.PureComponent {
view={query.view}
/>
)}
{metric == null &&
{!metric &&
hasBubbleChart(query.metric) && (
<MeasureOverviewContainer
branchLike={branchLike}

server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js → server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.tsx View File

@@ -17,9 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { withRouter, WithRouterProps } from 'react-router';
import App from './App';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import { getCurrentUser, getMetrics, getMetricsKey } from '../../../store/rootReducer';
@@ -28,31 +28,57 @@ import { getMeasuresAndMeta } from '../../../api/measures';
import { getLeakPeriod } from '../../../helpers/periods';
import { enhanceMeasure } from '../../../components/measure/utils';
import { getBranchLikeQuery } from '../../../helpers/branches';
/*:: import type { Component, Period } from '../types'; */
/*:: import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; */
import {
BranchLike,
ComponentMeasure,
CurrentUser,
Measure,
MeasureEnhanced,
Metric,
Period
} from '../../../app/types';

const mapStateToProps = state => ({
interface StateToProps {
currentUser: CurrentUser;
metrics: { [metric: string]: Metric };
metricsKey: string[];
}

interface DispatchToProps {
fetchMeasures: (
component: string,
metricsKey: string[],
branchLike?: BranchLike
) => Promise<{ component: ComponentMeasure; measures: MeasureEnhanced[]; leakPeriod?: Period }>;
fetchMetrics: () => void;
}

interface OwnProps {
branchLike?: BranchLike;
component: ComponentMeasure;
}

const mapStateToProps = (state: any): StateToProps => ({
currentUser: getCurrentUser(state),
metrics: getMetrics(state),
metricsKey: getMetricsKey(state)
});

function banQualityGate(component /*: Component */) /*: Array<Measure> */ {
const bannedMetrics = [];
if (!['VW', 'SVW'].includes(component.qualifier)) {
function banQualityGate({ measures = [], qualifier }: ComponentMeasure): Measure[] {
const bannedMetrics: string[] = [];
if (!['VW', 'SVW'].includes(qualifier)) {
bannedMetrics.push('alert_status');
}
if (component.qualifier === 'APP') {
if (qualifier === 'APP') {
bannedMetrics.push('releasability_rating', 'releasability_effort');
}
return component.measures.filter(measure => !bannedMetrics.includes(measure.metric));
return measures.filter(measure => !bannedMetrics.includes(measure.metric));
}

const fetchMeasures = (
component /*: string */,
metricsKey /*: Array<string> */,
branchLike /*: { id?: string; name: string } | void */
) => (dispatch, getState) => {
const fetchMeasures = (component: string, metricsKey: string[], branchLike?: BranchLike) => (
_dispatch: Dispatch<any>,
getState: () => any
) => {
if (metricsKey.length <= 0) {
return Promise.resolve({ component: {}, measures: [], leakPeriod: null });
}
@@ -60,21 +86,23 @@ const fetchMeasures = (
return getMeasuresAndMeta(component, metricsKey, {
additionalFields: 'periods',
...getBranchLikeQuery(branchLike)
}).then(r => {
const measures = banQualityGate(r.component).map(measure =>
}).then(({ component, periods }) => {
const measures = banQualityGate(component).map(measure =>
enhanceMeasure(measure, getMetrics(getState()))
);

const newBugs = measures.find(measure => measure.metric.key === 'new_bugs');
const applicationPeriods = newBugs ? [{ index: 1 }] : [];
const periods = r.component.qualifier === 'APP' ? applicationPeriods : r.periods;
return { component: r.component, measures, leakPeriod: getLeakPeriod(periods) };
const applicationPeriods = newBugs ? [{ index: 1 } as Period] : [];
const leakPeriod = getLeakPeriod(component.qualifier === 'APP' ? applicationPeriods : periods);
return { component, measures, leakPeriod };
}, throwGlobalError);
};

const mapDispatchToProps = { fetchMeasures, fetchMetrics };
const mapDispatchToProps: DispatchToProps = { fetchMeasures: fetchMeasures as any, fetchMetrics };

export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(App));
export default withRouter<OwnProps>(
connect<StateToProps, DispatchToProps, OwnProps & WithRouterProps>(
mapStateToProps,
mapDispatchToProps
)(App)
);

server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js → server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.tsx View File

@@ -17,22 +17,21 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';

/*:: type Props = {
className?: string,
current: ?number,
total: number
}; */
interface Props {
className?: string;
current?: number;
total: number;
}

export default function FilesCounter({ className, current, total } /*: Props */) {
export default function FilesCounter({ className, current, total }: Props) {
return (
<span className={className}>
<strong>
{current != null && (
{current !== undefined && (
<span>
{formatMeasure(current, 'INT')}
{' / '}

+ 2
- 2
server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx View File

@@ -24,10 +24,10 @@ import DateFromNow from '../../../components/intl/DateFromNow';
import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import Tooltip from '../../../components/controls/Tooltip';
import { getPeriodLabel, getPeriodDate, Period, PeriodMode } from '../../../helpers/periods';
import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { differenceInDays } from '../../../helpers/dates';
import { ComponentMeasure } from '../../../app/types';
import { ComponentMeasure, Period, PeriodMode } from '../../../app/types';

interface Props {
className?: string;

+ 3
- 5
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx View File

@@ -44,10 +44,10 @@ import {
isLoggedIn,
Metric,
Paging,
MeasureEnhanced
MeasureEnhanced,
Period
} from '../../../app/types';
import { RequestData } from '../../../helpers/request';
import { Period } from '../../../helpers/periods';

interface Props {
branchLike?: BranchLike;
@@ -328,10 +328,8 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
<MeasureHeader
branchLike={branchLike}
component={component}
components={this.state.components}
leakPeriod={this.props.leakPeriod}
// fall back to `undefined` to be compatible with typescript files where we compare with `=== undefined`
measure={measure || undefined}
measure={measure}
metric={metric}
secondaryMeasure={this.props.secondaryMeasure}
/>

+ 2
- 2
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.tsx View File

@@ -26,9 +26,9 @@ import {
Metric,
BranchLike,
CurrentUser,
MeasureEnhanced
MeasureEnhanced,
Period
} from '../../../app/types';
import { Period } from '../../../helpers/periods';

interface Props {
branchLike?: BranchLike;

server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js → server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { Link } from 'react-router';
import LeakPeriodLegend from './LeakPeriodLegend';
import HistoryIcon from '../../../components/icons-components/HistoryIcon';
@@ -29,21 +28,18 @@ import Tooltip from '../../../components/controls/Tooltip';
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { getMeasureHistoryUrl } from '../../../helpers/urls';
import { isDiffMetric } from '../../../helpers/measures';
/*:: import type { Component, Period } from '../types'; */
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
/*:: import type { Metric } from '../../../app/flow-types'; */
import { MeasureEnhanced, Metric, ComponentMeasure, BranchLike, Period } from '../../../app/types';

/*:: type Props = {|
branchLike?: { id?: string; name: string },
component: Component,
components: Array<Component>,
leakPeriod?: Period,
measure?: MeasureEnhanced,
metric: Metric,
secondaryMeasure: ?MeasureEnhanced
|}; */
interface Props {
branchLike?: BranchLike;
component: ComponentMeasure;
leakPeriod?: Period;
measure?: MeasureEnhanced;
metric: Metric;
secondaryMeasure?: MeasureEnhanced;
}

export default function MeasureHeader(props /*: Props*/) {
export default function MeasureHeader(props: Props) {
const { branchLike, component, leakPeriod, measure, metric, secondaryMeasure } = props;
const isDiff = isDiffMetric(metric.key);
const hasHistory = component.qualifier !== 'FIL' && component.qualifier !== 'UTS';
@@ -83,13 +79,14 @@ export default function MeasureHeader(props /*: Props*/) {
)}
</div>
<div className="measure-details-primary-actions">
{leakPeriod != null && (
{leakPeriod && (
<LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
)}
</div>
</div>
{secondaryMeasure &&
secondaryMeasure.metric.key === 'ncloc_language_distribution' && (
secondaryMeasure.metric.key === 'ncloc_language_distribution' &&
secondaryMeasure.value !== undefined && (
<div className="measure-details-secondary">
<LanguageDistributionContainer
alignTicks={true}

server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js → server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import Breadcrumbs from './Breadcrumbs';
import LeakPeriodLegend from './LeakPeriodLegend';
import MeasureFavoriteContainer from './MeasureFavoriteContainer';
@@ -29,44 +28,47 @@ import { getComponentLeaves } from '../../../api/components';
import { enhanceComponent, getBubbleMetrics, isFileType } from '../utils';
import { getBranchLikeQuery } from '../../../helpers/branches';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
/*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */
/*:: import type { Metric } from '../../../app/flow-types'; */
import {
BranchLike,
ComponentMeasure,
ComponentMeasureEnhanced,
CurrentUser,
Metric,
Paging,
Period
} from '../../../app/types';

/*:: type Props = {|
branchLike?: { id?: string; name: string },
className?: string,
component: Component,
currentUser: { isLoggedIn: boolean },
domain: string,
leakPeriod: Period,
loading: boolean,
metrics: { [string]: Metric },
rootComponent: Component,
updateLoading: ({ [string]: boolean }) => void,
updateSelected: string => void
|}; */
interface Props {
branchLike?: BranchLike;
className?: string;
component: ComponentMeasure;
currentUser: CurrentUser;
domain: string;
leakPeriod?: Period;
loading: boolean;
metrics: { [metric: string]: Metric };
rootComponent: ComponentMeasure;
updateLoading: (param: { [key: string]: boolean }) => void;
updateSelected: (component: string) => void;
}

/*:: type State = {
components: Array<ComponentEnhanced>,
paging?: Paging
}; */
interface State {
components: ComponentMeasureEnhanced[];
paging?: Paging;
}

const BUBBLES_LIMIT = 500;

export default class MeasureOverview extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
state /*: State */ = {
components: [],
paging: null
};
export default class MeasureOverview extends React.PureComponent<Props, State> {
mounted = false;
state: State = { components: [] };

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

componentWillReceiveProps(nextProps /*: Props */) {
componentWillReceiveProps(nextProps: Props) {
if (
nextProps.component !== this.props.component ||
nextProps.metrics !== this.props.metrics ||
@@ -80,16 +82,16 @@ export default class MeasureOverview extends React.PureComponent {
this.mounted = false;
}

fetchComponents = (props /*: Props */) => {
fetchComponents = (props: Props) => {
const { branchLike, component, domain, metrics } = props;
if (isFileType(component)) {
this.setState({ components: [], paging: null });
this.setState({ components: [], paging: undefined });
return;
}
const { x, y, size, colors } = getBubbleMetrics(domain, metrics);
const metricsKey = [x.key, y.key, size.key];
if (colors) {
metricsKey.push(colors.map(metric => metric.key));
metricsKey.push(...colors.map(metric => metric.key));
}
const options = {
...getBranchLikeQuery(branchLike),
@@ -105,7 +107,9 @@ export default class MeasureOverview extends React.PureComponent {
if (domain === this.props.domain) {
if (this.mounted) {
this.setState({
components: r.components.map(component => enhanceComponent(component, null, metrics)),
components: r.components.map(component =>
enhanceComponent(component, undefined, metrics)
),
paging: r.paging
});
}
@@ -171,7 +175,7 @@ export default class MeasureOverview extends React.PureComponent {
</div>
<div className="layout-page-main-inner measure-details-content">
<div className="clearfix big-spacer-bottom">
{leakPeriod != null && (
{leakPeriod && (
<LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />
)}
</div>

server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js → server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx View File

@@ -17,49 +17,43 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { InjectedRouter } from 'react-router';
import MeasureOverview from './MeasureOverview';
import { getComponentShow } from '../../../api/components';
import { getProjectUrl } from '../../../helpers/urls';
import { isViewType } from '../utils';
import { isViewType, Query } from '../utils';
import { getBranchLikeQuery } from '../../../helpers/branches';
/*:: import type { Component, Period, Query } from '../types'; */
/*:: import type { RawQuery } from '../../../helpers/query'; */
/*:: import type { Metric } from '../../../app/flow-types'; */
import { BranchLike, ComponentMeasure, CurrentUser, Metric, Period } from '../../../app/types';

/*:: type Props = {|
branchLike?: { id?: string; name: string },
className?: string,
rootComponent: Component,
currentUser: { isLoggedIn: boolean },
domain: string,
leakPeriod: Period,
metrics: { [string]: Metric },
router: {
push: ({ pathname: string, query?: RawQuery }) => void
},
selected: ?string,
updateQuery: Query => void
|}; */
interface Props {
branchLike?: BranchLike;
className?: string;
currentUser: CurrentUser;
domain: string;
leakPeriod?: Period;
metrics: { [metric: string]: Metric };
rootComponent: ComponentMeasure;
router: InjectedRouter;
selected?: string;
updateQuery: (query: Partial<Query>) => void;
}

/*:: type State = {
component: ?Component,
loading: {
component: boolean,
bubbles: boolean
}
}; */
interface LoadingState {
bubbles: boolean;
component: boolean;
}

export default class MeasureOverviewContainer extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
state /*: State */ = {
component: null,
loading: {
component: false,
bubbles: false
}
interface State {
component?: ComponentMeasure;
loading: LoadingState;
}

export default class MeasureOverviewContainer extends React.PureComponent<Props, State> {
mounted = false;

state: State = {
loading: { bubbles: false, component: false }
};

componentDidMount() {
@@ -67,7 +61,7 @@ export default class MeasureOverviewContainer extends React.PureComponent {
this.fetchComponent(this.props);
}

componentWillReceiveProps(nextProps /*: Props */) {
componentWillReceiveProps(nextProps: Props) {
const { component } = this.state;
const componentChanged =
!component ||
@@ -82,7 +76,7 @@ export default class MeasureOverviewContainer extends React.PureComponent {
this.mounted = false;
}

fetchComponent = ({ branchLike, rootComponent, selected } /*: Props */) => {
fetchComponent = ({ branchLike, rootComponent, selected }: Props) => {
if (!selected || rootComponent.key === selected) {
this.setState({ component: rootComponent });
this.updateLoading({ component: false });
@@ -100,18 +94,18 @@ export default class MeasureOverviewContainer extends React.PureComponent {
);
};

updateLoading = (loading /*: { [string]: boolean } */) => {
updateLoading = (loading: Partial<LoadingState>) => {
if (this.mounted) {
this.setState(state => ({ loading: { ...state.loading, ...loading } }));
}
};

updateSelected = (component /*: string */) => {
updateSelected = (component: string) => {
if (this.state.component && isViewType(this.state.component)) {
this.props.router.push(getProjectUrl(component));
} else {
this.props.updateQuery({
selected: component !== this.props.rootComponent.key ? component : null
selected: component !== this.props.rootComponent.key ? component : undefined
});
}
};

server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js → server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.tsx View File

@@ -17,11 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';

export default function MetricNotFound({ className } /*: { className?: string } */) {
export default function MetricNotFound({ className }: { className?: string }) {
return (
<div className={className}>
<div className="alert alert-danger">{translate('component_measures.not_found')}</div>

server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js → server/sonar-web/src/main/js/apps/component-measures/components/PageActions.tsx View File

@@ -17,23 +17,22 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import FilesCounter from './FilesCounter';
import { translate } from '../../../helpers/l10n';
/*:: import type { Paging } from '../types'; */
import { Paging } from '../../../app/types';

/*:: type Props = {|
current: ?number,
isFile: ?boolean,
paging: ?Paging,
totalLoadedComponents?: number,
view?: string
|}; */
interface Props {
current?: number;
isFile?: boolean;
paging?: Paging;
totalLoadedComponents?: number;
view?: string;
}

export default function PageActions(props /*: Props */) {
export default function PageActions(props: Props) {
const { isFile, paging, totalLoadedComponents } = props;
const showShortcuts = ['list', 'tree'].includes(props.view);
const showShortcuts = props.view && ['list', 'tree'].includes(props.view);
return (
<div className="pull-right">
{!isFile && showShortcuts && renderShortcuts()}

server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js → server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx View File

@@ -17,36 +17,42 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
/* eslint-disable camelcase */
import * as React from 'react';
import { shallow } from 'enzyme';
import App from '../App';

const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' };

const METRICS = {
lines_to_cover: {
id: '1',
key: 'lines_to_cover',
type: 'INT',
name: 'Lines to Cover',
domain: 'Coverage'
},
coverage: { key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
coverage: { id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
duplicated_lines_density: {
id: '3',
key: 'duplicated_lines_density',
type: 'PERCENT',
name: 'Duplicated Lines (%)',
domain: 'Duplications'
},
new_bugs: { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
new_bugs: { id: '4', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
};

const PROPS = {
branch: { isMain: true, name: 'master' },
component: { key: 'foo' },
component: COMPONENT,
currentUser: { isLoggedIn: false },
location: { pathname: '/component_measures', query: { metric: 'coverage' } },
fetchMeasures: () => Promise.resolve({ measures: [] }),
fetchMetrics: () => {},
fetchMeasures: jest.fn().mockResolvedValue({ component: COMPONENT, measures: [] }),
fetchMetrics: jest.fn(),
metrics: METRICS,
metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'],
router: { push: () => {} }
router: { push: jest.fn() } as any
};

it('should render correctly', () => {

server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js → server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.tsx View File

@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import FilesCounter from '../FilesCounter';

@@ -26,5 +26,5 @@ it('should display x files on y total', () => {
});

it('should display only total of files', () => {
expect(shallow(<FilesCounter current={null} total={123455} />)).toMatchSnapshot();
expect(shallow(<FilesCounter current={undefined} total={123455} />)).toMatchSnapshot();
});

+ 1
- 2
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx View File

@@ -20,9 +20,8 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import LeakPeriodLegend from '../LeakPeriodLegend';
import { PeriodMode, Period } from '../../../../helpers/periods';
import { differenceInDays } from '../../../../helpers/dates';
import { ComponentMeasure } from '../../../../app/types';
import { ComponentMeasure, Period, PeriodMode } from '../../../../app/types';

jest.mock('../../../../helpers/dates', () => {
const dates = require.requireActual('../../../../helpers/dates');

server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js → server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx View File

@@ -17,11 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import MeasureHeader from '../MeasureHeader';
import { PeriodMode } from '../../../../app/types';

const METRIC = {
id: '1',
key: 'reliability_rating',
type: 'RATING',
name: 'Reliability Rating'
@@ -35,6 +37,7 @@ const MEASURE = {
};

const LEAK_METRIC = {
id: '2',
key: 'new_reliability_rating',
type: 'RATING',
name: 'Reliability Rating on New Code'
@@ -49,28 +52,23 @@ const LEAK_MEASURE = {
const SECONDARY = {
value: 'java=175123;js=26382',
metric: {
id: '3',
key: 'ncloc_language_distribution',
type: 'DATA',
name: 'Lines of Code Per Language'
},
leak: null
}
};

const PROPS = {
component: { key: 'foo', qualifier: 'TRK' },
components: [],
handleSelect: () => {},
component: { key: 'foo', name: 'Foo', qualifier: 'TRK' },
leakPeriod: {
date: '2017-05-16T13:50:02+0200',
index: 1,
mode: 'previous_version',
mode: PeriodMode.PreviousVersion,
parameter: '6,4'
},
measure: MEASURE,
metric: METRIC,
paging: null,
secondaryMeasure: null,
selectedIdx: null
metric: METRIC
};

it('should render correctly', () => {
@@ -109,20 +107,6 @@ it('should display secondary measure too', () => {
expect(wrapper.find('Connect(LanguageDistribution)')).toHaveLength(1);
});

it('should display correctly for open file', () => {
const wrapper = shallow(
<MeasureHeader
{...PROPS}
component={{ key: 'bar', qualifier: 'FIL' }}
components={[{ key: 'foo' }, { key: 'bar' }, { key: 'baz' }]}
selectedIdx={1}
/>
);
expect(wrapper.find('.measure-details-primary-actions')).toMatchSnapshot();
wrapper.setProps({ components: [{ key: 'foo' }, { key: 'bar' }] });
expect(wrapper.find('.measure-details-primary-actions')).toMatchSnapshot();
});

it('should work with measure without value', () => {
expect(shallow(<MeasureHeader {...PROPS} measure={undefined} />)).toMatchSnapshot();
});

server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js → server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.tsx View File

@@ -17,10 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import PageActions from '../PageActions';

const PAGING = {
pageIndex: 1,
pageSize: 100,
total: 120
};

it('should display correctly for a project', () => {
expect(
shallow(<PageActions isFile={false} totalLoadedComponents={20} view="list" />)
@@ -46,7 +52,7 @@ it('should display the total of files', () => {
<PageActions
current={12}
isFile={false}
paging={{ total: 120 }}
paging={PAGING}
totalLoadedComponents={20}
view="treemap"
/>
@@ -57,7 +63,7 @@ it('should display the total of files', () => {
<PageActions
current={12}
isFile={true}
paging={{ total: 120 }}
paging={PAGING}
totalLoadedComponents={20}
view="list"
/>

server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap → server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -18,11 +18,37 @@ exports[`should render correctly 1`] = `
/>
<MeasureContentContainer
className="layout-page-main"
fetchMeasures={[Function]}
leakPeriod={null}
currentUser={
Object {
"isLoggedIn": false,
}
}
fetchMeasures={
[MockFunction] {
"calls": Array [
Array [
"foo",
Array [
"lines_to_cover",
"coverage",
"duplicated_lines_density",
"new_bugs",
],
undefined,
],
],
"results": Array [
Object {
"isThrow": false,
"value": Promise {},
},
],
}
}
metric={
Object {
"domain": "Coverage",
"id": "2",
"key": "coverage",
"name": "Coverage",
"type": "PERCENT",
@@ -32,24 +58,28 @@ exports[`should render correctly 1`] = `
Object {
"coverage": Object {
"domain": "Coverage",
"id": "2",
"key": "coverage",
"name": "Coverage",
"type": "PERCENT",
},
"duplicated_lines_density": Object {
"domain": "Duplications",
"id": "3",
"key": "duplicated_lines_density",
"name": "Duplicated Lines (%)",
"type": "PERCENT",
},
"lines_to_cover": Object {
"domain": "Coverage",
"id": "1",
"key": "lines_to_cover",
"name": "Lines to Cover",
"type": "INT",
},
"new_bugs": Object {
"domain": "Reliability",
"id": "4",
"key": "new_bugs",
"name": "New Bugs",
"type": "INT",
@@ -59,11 +89,13 @@ exports[`should render correctly 1`] = `
rootComponent={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}
router={
Object {
"push": [Function],
"push": [MockFunction],
}
}
selected=""

server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap → server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.tsx.snap View File


server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap → server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap View File

@@ -1,53 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display correctly for open file 1`] = `
<div
className="measure-details-primary-actions"
>
<LeakPeriodLegend
className="spacer-left"
component={
Object {
"key": "bar",
"qualifier": "FIL",
}
}
period={
Object {
"date": "2017-05-16T13:50:02+0200",
"index": 1,
"mode": "previous_version",
"parameter": "6,4",
}
}
/>
</div>
`;

exports[`should display correctly for open file 2`] = `
<div
className="measure-details-primary-actions"
>
<LeakPeriodLegend
className="spacer-left"
component={
Object {
"key": "bar",
"qualifier": "FIL",
}
}
period={
Object {
"date": "2017-05-16T13:50:02+0200",
"index": 1,
"mode": "previous_version",
"parameter": "6,4",
}
}
/>
</div>
`;

exports[`should render correctly 1`] = `
<div
className="measure-details-header big-spacer-bottom"
@@ -104,6 +56,7 @@ exports[`should render correctly 1`] = `
component={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}
@@ -157,6 +110,7 @@ exports[`should render correctly for leak 1`] = `
component={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}
@@ -250,6 +204,7 @@ exports[`should work with measure without value 1`] = `
component={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}

server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap → server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.tsx.snap View File


+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts View File

@@ -23,7 +23,7 @@ export const bubbles: {
y: string;
size: string;
colors?: string[];
yDomain?: number[];
yDomain?: [number, number];
};
} = {
Reliability: {

+ 6
- 1
server/sonar-web/src/main/js/apps/component-measures/config/domains.ts View File

@@ -17,7 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
export const domains: { [domain: string]: { categories?: string[]; order: string[] } } = {

interface Domains {
[domain: string]: { categories?: string[]; order: string[] };
}

export const domains: Domains = {
Reliability: {
categories: ['new_code_category', 'overall_category'],
order: [

server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js → server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx View File

@@ -17,10 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import EmptyResult from './EmptyResult';
import OriginalBubbleChart from '../../../components/charts/BubbleChart';
import OriginalBubbleChart, { BubbleItem } from '../../../components/charts/BubbleChart';
import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
@@ -32,43 +31,31 @@ import {
} from '../../../helpers/l10n';
import { getBubbleMetrics, getBubbleYDomain, isProjectOverview } from '../utils';
import { RATING_COLORS } from '../../../helpers/constants';
/*:: import type { Component, ComponentEnhanced } from '../types'; */
/*:: import type { Metric } from '../../../app/flow-types'; */
import { ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types';

const HEIGHT = 500;

/*:: type Props = {|
component: Component,
components: Array<ComponentEnhanced>,
domain: string,
metrics: { [string]: Metric },
updateSelected: string => void
|}; */

export default class BubbleChart extends React.PureComponent {
/*:: props: Props; */
interface Props {
component: ComponentMeasure;
components: ComponentMeasureEnhanced[];
domain: string;
metrics: { [metric: string]: Metric };
updateSelected: (component: string) => void;
}

getMeasureVal = (component /*: ComponentEnhanced */, metric /*: Metric */) => {
export default class BubbleChart extends React.PureComponent<Props> {
getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => {
const measure = component.measures.find(measure => measure.metric.key === metric.key);
if (measure) {
return Number(isDiffMetric(metric.key) ? measure.leak : measure.value);
if (!measure) {
return undefined;
}
return Number(isDiffMetric(metric.key) ? measure.leak : measure.value);
};

getTooltip(
componentName /*: string */,
values /*: {
x: number,
y: number,
size: number,
colors: ?Array<?number>
}*/,
metrics /*: {
x: Metric ,
y: Metric ,
size: Metric ,
colors: ?Array<Metric>
}*/
componentName: string,
values: { x: number; y: number; size: number; colors?: Array<number | undefined> },
metrics: { x: Metric; y: Metric; size: Metric; colors?: Array<Metric> }
) {
const inner = [
componentName,
@@ -76,10 +63,11 @@ export default class BubbleChart extends React.PureComponent {
`${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
`${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`
];
if (values.colors && metrics.colors) {
metrics.colors.forEach((metric, idx) => {
// $FlowFixMe colors is always defined at this point
const colorValue = values.colors[idx];
const { colors: valuesColors } = values;
const { colors: metricColors } = metrics;
if (valuesColors && metricColors) {
metricColors.forEach((metric, idx) => {
const colorValue = valuesColors[idx];
if (colorValue || colorValue === 0) {
inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
}
@@ -97,10 +85,10 @@ export default class BubbleChart extends React.PureComponent {
);
}

handleBubbleClick = (component /*: ComponentEnhanced */) =>
handleBubbleClick = (component: ComponentMeasureEnhanced) =>
this.props.updateSelected(component.refKey || component.key);

getDescription(domain /*: string */) {
getDescription(domain: string) {
const description = `component_measures.overview.${domain}.description`;
const translatedDescription = translate(description);
if (description === translatedDescription) {
@@ -109,14 +97,7 @@ export default class BubbleChart extends React.PureComponent {
return translatedDescription;
}

renderBubbleChart(
metrics /*: {
x: Metric ,
y: Metric ,
size: Metric ,
colors: ?Array<Metric>
}*/
) {
renderBubbleChart(metrics: { x: Metric; y: Metric; size: Metric; colors?: Metric[] }) {
const items = this.props.components
.map(component => {
const x = this.getMeasureVal(component, metrics.x);
@@ -125,25 +106,27 @@ export default class BubbleChart extends React.PureComponent {
const colors =
metrics.colors && metrics.colors.map(metric => this.getMeasureVal(component, metric));
if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
return null;
return undefined;
}
return {
x,
y,
size,
color:
colors != null ? RATING_COLORS[Math.max(...colors.filter(Boolean)) - 1] : undefined,
link: component,
colors !== undefined
? RATING_COLORS[Math.max(...colors.filter(Boolean) as number[]) - 1]
: undefined,
data: component,
tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics)
};
})
.filter(Boolean);
.filter(Boolean) as BubbleItem<ComponentMeasureEnhanced>[];

const formatXTick = tick => formatMeasure(tick, metrics.x.type);
const formatYTick = tick => formatMeasure(tick, metrics.y.type);
const formatXTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.x.type);
const formatYTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.y.type);

return (
<OriginalBubbleChart
<OriginalBubbleChart<ComponentMeasureEnhanced>
formatXTick={formatXTick}
formatYTick={formatYTick}
height={HEIGHT}
@@ -155,11 +138,7 @@ export default class BubbleChart extends React.PureComponent {
);
}

renderChartHeader(
domain /*: string */,
sizeMetric /*: Metric */,
colorsMetric /*: ?Array<Metric> */
) {
renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric?: Metric[]) {
const title = isProjectOverview(domain)
? translate('component_measures.overview', domain, 'title')
: translateWithParameters(

+ 7
- 2
server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.tsx View File

@@ -20,8 +20,13 @@
import * as React from 'react';
import * as key from 'keymaster';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { BranchLike, ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types';
import { Period } from '../../../helpers/periods';
import {
BranchLike,
ComponentMeasure,
ComponentMeasureEnhanced,
Metric,
Period
} from '../../../app/types';

interface Props {
branchLike?: BranchLike;

server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js → server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { Link } from 'react-router';
import LinkIcon from '../../../components/icons-components/LinkIcon';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
@@ -27,80 +26,72 @@ import { splitPath } from '../../../helpers/path';
import {
getPathUrlAsString,
getBranchLikeUrl,
getLongLivingBranchUrl,
getComponentDrilldownUrlWithSelection
getComponentDrilldownUrlWithSelection,
getProjectUrl
} from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
/*:: import type { Component, ComponentEnhanced } from '../types'; */
/*:: import type { Metric } from '../../../app/flow-types'; */
import { BranchLike, ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types';

/*:: type Props = {
branchLike?: { id?: string; name: string },
component: ComponentEnhanced,
onClick: string => void,
metric: Metric,
rootComponent: Component
}; */

export default class ComponentCell extends React.PureComponent {
/*:: props: Props; */
interface Props {
branchLike?: BranchLike;
component: ComponentMeasureEnhanced;
onClick: (component: string) => void;
metric: Metric;
rootComponent: ComponentMeasure;
}

handleClick = (e /*: MouseEvent */) => {
const isLeftClickEvent = e.button === 0;
const isModifiedEvent = !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
export default class ComponentCell extends React.PureComponent<Props> {
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
const isLeftClickEvent = event.button === 0;
const isModifiedEvent = !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);

if (isLeftClickEvent && !isModifiedEvent) {
e.preventDefault();
event.preventDefault();
this.props.onClick(this.props.component.key);
}
};

renderInner() {
renderInner(componentKey: string) {
const { component } = this.props;
let head = '';
let tail = component.name;
let branch = null;
let branchComponent = null;

if (['DIR', 'FIL', 'UTS'].includes(component.qualifier)) {
const parts = splitPath(component.path);
({ head, tail } = parts);
if (['DIR', 'FIL', 'UTS'].includes(component.qualifier) && component.path) {
({ head, tail } = splitPath(component.path));
}

if (this.props.rootComponent.qualifier === 'APP') {
branch = (
<React.Fragment>
branchComponent = (
<>
{component.branch ? (
<React.Fragment>
<>
<LongLivingBranchIcon className="spacer-left little-spacer-right" />
<span className="note">{component.branch}</span>
</React.Fragment>
</>
) : (
<span className="spacer-left outline-badge">{translate('branches.main_branch')}</span>
)}
</React.Fragment>
</>
);
}
return (
<span title={component.refKey || component.key}>
<span title={componentKey}>
<QualifierIcon qualifier={component.qualifier} />
&nbsp;
{head.length > 0 && <span className="note">{head}/</span>}
<span>{tail}</span>
{branch}
{branchComponent}
</span>
);
}

render() {
const { branchLike, component, metric, rootComponent } = this.props;
const to =
this.props.rootComponent.qualifier === 'APP'
? getLongLivingBranchUrl(component.refKey, component.branch)
: getBranchLikeUrl(component.refKey, branchLike);
return (
<td className="measure-details-component-cell">
<div className="text-ellipsis">
{component.refKey == null ? (
{!component.refKey ? (
<a
className="link-no-underline"
href={getPathUrlAsString(
@@ -113,17 +104,21 @@ export default class ComponentCell extends React.PureComponent {
)}
id={'component-measures-component-link-' + component.key}
onClick={this.handleClick}>
{this.renderInner()}
{this.renderInner(component.key)}
</a>
) : (
<Link
className="link-no-underline"
id={'component-measures-component-link-' + component.key}
to={to}>
id={'component-measures-component-link-' + component.refKey}
to={
this.props.rootComponent.qualifier === 'APP'
? getProjectUrl(component.refKey, component.branch)
: getBranchLikeUrl(component.refKey, branchLike)
}>
<span className="big-spacer-right">
<LinkIcon />
</span>
{this.renderInner()}
{this.renderInner(component.refKey)}
</Link>
)}
</div>

server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js → server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx View File

@@ -17,25 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import * as React from 'react';
import * as classNames from 'classnames';
import ComponentCell from './ComponentCell';
import MeasureCell from './MeasureCell';
/*:: import type { Component, ComponentEnhanced } from '../types'; */
/*:: import type { Metric } from '../../../app/flow-types'; */
import { ComponentMeasure, Metric, ComponentMeasureEnhanced, BranchLike } from '../../../app/types';

/*:: type Props = {|
branchLike?: { id?: string; name: string },
component: ComponentEnhanced,
isSelected: boolean,
onClick: string => void,
otherMetrics: Array<Metric>,
metric: Metric,
rootComponent: Component
|}; */
interface Props {
branchLike?: BranchLike;
component: ComponentMeasureEnhanced;
isSelected: boolean;
onClick: (component: string) => void;
otherMetrics: Metric[];
metric: Metric;
rootComponent: ComponentMeasure;
}

export default function ComponentsListRow(props /*: Props */) {
export default function ComponentsListRow(props: Props) {
const { branchLike, component, rootComponent } = props;
const otherMeasures = props.otherMetrics.map(metric => {
const measure = component.measures.find(measure => measure.metric.key === metric.key);

server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js → server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx View File

@@ -17,22 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import Measure from '../../../components/measure/Measure';
import { isDiffMetric } from '../../../helpers/measures';
/*:: import type { ComponentEnhanced } from '../types'; */
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
/*:: import type { Metric } from '../../../app/flow-types'; */
import { Metric, MeasureEnhanced, ComponentMeasureEnhanced } from '../../../app/types';

/*:: type Props = {
component: ComponentEnhanced,
measure?: MeasureEnhanced,
metric: Metric
}; */
interface Props {
component: ComponentMeasureEnhanced;
measure?: MeasureEnhanced;
metric: Metric;
}

export default function MeasureCell({ component, measure, metric } /*: Props */) {
const getValue = (item /*: { leak?: ?string; value?: string } */) =>
export default function MeasureCell({ component, measure, metric }: Props) {
const getValue = (item: { leak?: string; value?: string }) =>
isDiffMetric(metric.key) ? item.leak : item.value;

const value = getValue(measure || component);

server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.js → server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/MeasureCell-test.tsx View File

@@ -17,19 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import MeasureCell from '../MeasureCell';

describe('should correctly take the value', () => {
const renderAndTakeValue = props =>
const renderAndTakeValue = (props: any) =>
shallow(<MeasureCell {...props} />)
.find('Measure')
.prop('value');

it('absolute value', () => {
const component = { value: '123' };
const metric = { key: 'coverage' };
const metric = { id: '1', key: 'coverage' };
const measure = { value: '567' };

expect(renderAndTakeValue({ component, metric })).toEqual('123');
@@ -38,7 +38,7 @@ describe('should correctly take the value', () => {

it('leak value', () => {
const component = { leak: '234' };
const metric = { key: 'new_coverage' };
const metric = { id: '1', key: 'new_coverage' };
const measure = { leak: '678' };

expect(renderAndTakeValue({ component, metric })).toEqual('234');

server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js → server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import FacetMeasureValue from './FacetMeasureValue';
import BubblesIcon from '../../../components/icons-components/BubblesIcon';
import FacetBox from '../../../components/facet/FacetBox';
@@ -38,26 +37,22 @@ import {
getLocalizedMetricName,
translate
} from '../../../helpers/l10n';
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
import { MeasureEnhanced } from '../../../app/types';

/*:: type Props = {|
onChange: (metric: string) => void,
onToggle: (property: string) => void,
open: boolean,
domain: { name: string, measures: Array<MeasureEnhanced> },
selected: string
|}; */

export default class DomainFacet extends React.PureComponent {
/*:: props: Props; */
interface Props {
domain: { name: string; measures: MeasureEnhanced[] };
onChange: (metric: string) => void;
onToggle: (property: string) => void;
open: boolean;
selected: string;
}

handleHeaderClick = () => this.props.onToggle(this.props.domain.name);
export default class DomainFacet extends React.PureComponent<Props> {
handleHeaderClick = () => {
this.props.onToggle(this.props.domain.name);
};

hasFacetSelected = (
domain /*: { name: string } */,
measures /*: Array<MeasureEnhanced> */,
selected /*: string */
) => {
hasFacetSelected = (domain: { name: string }, measures: MeasureEnhanced[], selected: string) => {
const measureSelected = measures.find(measure => measure.metric.key === selected);
const overviewSelected = domain.name === selected && hasBubbleChart(domain.name);
return measureSelected || overviewSelected;
@@ -73,8 +68,9 @@ export default class DomainFacet extends React.PureComponent {
return overviewSelected ? [translate('component_measures.domain_overview')] : [];
};

renderItemFacetStat = (item /*: MeasureEnhanced */) =>
hasFacetStat(item.metric.key) ? <FacetMeasureValue measure={item} /> : null;
renderItemFacetStat = (item: MeasureEnhanced) => {
return hasFacetStat(item.metric.key) ? <FacetMeasureValue measure={item} /> : null;
};

renderItemsFacet = () => {
const { domain, selected } = this.props;
@@ -110,6 +106,7 @@ export default class DomainFacet extends React.PureComponent {
}
onClick={this.props.onChange}
stat={this.renderItemFacetStat(item)}
tooltip={translateMetric(item.metric)}
value={item.metric.key}
/>
)
@@ -133,6 +130,7 @@ export default class DomainFacet extends React.PureComponent {
}
onClick={this.props.onChange}
stat={<BubblesIcon size={14} />}
tooltip={translate('component_measures.domain_overview')}
value={domain.name}
/>
);

server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js → server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx View File

@@ -17,13 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import Measure from '../../../components/measure/Measure';
import { isDiffMetric } from '../../../helpers/measures';
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
import { MeasureEnhanced } from '../../../app/types';

export default function FacetMeasureValue({ measure } /*: { measure: MeasureEnhanced } */) {
interface Props {
measure: MeasureEnhanced;
}

export default function FacetMeasureValue({ measure }: Props) {
if (isDiffMetric(measure.metric.key)) {
return (
<div

server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js → server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx View File

@@ -17,20 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import FacetBox from '../../../components/facet/FacetBox';
import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import { translate } from '../../../helpers/l10n';

/*:: type Props = {|
onChange: (metric: string) => void,
selected: string,
value: string
|}; */
interface Props {
onChange: (metric: string) => void;
selected: string;
value: string;
}

export default function ProjectOverviewFacet({ value, selected, onChange } /*: Props */) {
export default function ProjectOverviewFacet({ value, selected, onChange }: Props) {
const facetName = translate('component_measures.overview', value, 'facet');
return (
<FacetBox property={value}>
@@ -39,12 +38,9 @@ export default function ProjectOverviewFacet({ value, selected, onChange } /*: P
active={value === selected}
disabled={false}
key={value}
name={
<strong id={`measure-overview-${value}-name`} title={facetName}>
{facetName}
</strong>
}
name={<strong id={`measure-overview-${value}-name`}>{facetName}</strong>}
onClick={onChange}
tooltip={facetName}
value={value}
/>
</FacetItemsList>

server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js → server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx View File

@@ -17,42 +17,39 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import ProjectOverviewFacet from './ProjectOverviewFacet';
import DomainFacet from './DomainFacet';
import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW } from '../utils';
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
/*:: import type { Query } from '../types'; */
import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW, Query } from '../utils';
import { MeasureEnhanced } from '../../../app/types';

/*:: type Props = {|
measures: Array<MeasureEnhanced>,
selectedMetric: string,
updateQuery: Query => void
|}; */

/*:: type State = {|
openFacets: { [string]: boolean }
|}; */
interface Props {
measures: MeasureEnhanced[];
selectedMetric: string;
updateQuery: (query: Query) => void;
}

export default class Sidebar extends React.PureComponent {
/*:: props: Props; */
/*:: state: State; */
interface State {
openFacets: { [metric: string]: boolean };
}

constructor(props /*: Props */) {
export default class Sidebar extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { openFacets: this.getOpenFacets({}, props) };
}

componentWillReceiveProps(nextProps /*: Props */) {
componentWillReceiveProps(nextProps: Props) {
if (nextProps.selectedMetric !== this.props.selectedMetric) {
this.setState(state => this.getOpenFacets(state.openFacets, nextProps));
this.setState(({ openFacets }) => ({
openFacets: this.getOpenFacets(openFacets, nextProps)
}));
}
}

getOpenFacets = (
openFacets /*: { [string]: boolean } */,
{ measures, selectedMetric } /*: Props */
openFacets: { [metric: string]: boolean },
{ measures, selectedMetric }: Props
) => {
const newOpenFacets = { ...openFacets };
const measure = measures.find(measure => measure.metric.key === selectedMetric);
@@ -64,15 +61,15 @@ export default class Sidebar extends React.PureComponent {
return newOpenFacets;
};

toggleFacet = (name /*: string */) => {
this.setState(({ openFacets } /*: State */) => ({
toggleFacet = (name: string) => {
this.setState(({ openFacets }) => ({
openFacets: { ...openFacets, [name]: !openFacets[name] }
}));
};

resetSelection = (metric /*: string */) => ({ selected: null, view: getDefaultView(metric) });
resetSelection = (metric: string) => ({ selected: undefined, view: getDefaultView(metric) });

changeMetric = (metric /*: string */) =>
changeMetric = (metric: string) =>
this.props.updateQuery({ metric, ...this.resetSelection(metric) });

render() {

server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js → server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import DomainFacet from '../DomainFacet';

@@ -27,6 +26,7 @@ const DOMAIN = {
measures: [
{
metric: {
id: '1',
key: 'bugs',
type: 'INT',
name: 'Bugs',
@@ -38,6 +38,7 @@ const DOMAIN = {
},
{
metric: {
id: '2',
key: 'new_bugs',
type: 'INT',
name: 'New Bugs',
@@ -75,7 +76,7 @@ it('should not display subtitles of new measures if there is none', () => {
name: 'Reliability',
measures: [
{
metric: { key: 'bugs', type: 'INT', name: 'Bugs', domain: 'Reliability' },
metric: { id: '1', key: 'bugs', type: 'INT', name: 'Bugs', domain: 'Reliability' },
value: '5'
}
]
@@ -99,7 +100,7 @@ it('should not display subtitles of new measures if there is none, even on last
name: 'Reliability',
measures: [
{
metric: { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' },
metric: { id: '2', key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' },
value: '5'
}
]

server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.js → server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetMeasureValue-test.tsx View File

@@ -17,13 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import FacetMeasureValue from '../FacetMeasureValue';

const MEASURE = {
metric: {
id: '1',
key: 'bugs',
type: 'INT',
name: 'Bugs',
@@ -35,6 +35,7 @@ const MEASURE = {
};
const LEAK_MEASURE = {
metric: {
id: '2',
key: 'new_bugs',
type: 'INT',
name: 'New Bugs',

server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js → server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx View File

@@ -17,13 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import Sidebar from '../Sidebar';

const MEASURES = [
{
metric: {
id: '1',
key: 'lines_to_cover',
type: 'INT',
name: 'Lines to Cover',
@@ -35,6 +36,7 @@ const MEASURES = [
},
{
metric: {
id: '2',
key: 'coverage',
type: 'PERCENT',
name: 'Coverage',
@@ -46,6 +48,7 @@ const MEASURES = [
},
{
metric: {
id: '3',
key: 'duplicated_lines_density',
type: 'PERCENT',
name: 'Duplicated Lines (%)',
@@ -70,8 +73,8 @@ it('should display two facets', () => {
it('should correctly toggle facets', () => {
const wrapper = shallow(<Sidebar {...PROPS} />);
expect(wrapper.state('openFacets').bugs).toBeUndefined();
wrapper.instance().toggleFacet('bugs');
(wrapper.instance() as Sidebar).toggleFacet('bugs');
expect(wrapper.state('openFacets').bugs).toBeTruthy();
wrapper.instance().toggleFacet('bugs');
(wrapper.instance() as Sidebar).toggleFacet('bugs');
expect(wrapper.state('openFacets').bugs).toBeFalsy();
});

server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap → server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.tsx.snap View File

@@ -30,6 +30,7 @@ exports[`should display facet item list 1`] = `
size={14}
/>
}
tooltip="component_measures.domain_overview"
value="Reliability"
/>
<span
@@ -64,6 +65,7 @@ exports[`should display facet item list 1`] = `
"leak": "5",
"metric": Object {
"domain": "Reliability",
"id": "2",
"key": "new_bugs",
"name": "New Bugs",
"type": "INT",
@@ -78,6 +80,7 @@ exports[`should display facet item list 1`] = `
}
/>
}
tooltip="New Bugs"
value="new_bugs"
/>
<span
@@ -112,6 +115,7 @@ exports[`should display facet item list 1`] = `
"leak": "5",
"metric": Object {
"domain": "Reliability",
"id": "1",
"key": "bugs",
"name": "Bugs",
"type": "INT",
@@ -127,6 +131,7 @@ exports[`should display facet item list 1`] = `
}
/>
}
tooltip="Bugs"
value="bugs"
/>
</FacetItemsList>
@@ -167,6 +172,7 @@ exports[`should display facet item list with bugs selected 1`] = `
size={14}
/>
}
tooltip="component_measures.domain_overview"
value="Reliability"
/>
<span
@@ -201,6 +207,7 @@ exports[`should display facet item list with bugs selected 1`] = `
"leak": "5",
"metric": Object {
"domain": "Reliability",
"id": "2",
"key": "new_bugs",
"name": "New Bugs",
"type": "INT",
@@ -215,6 +222,7 @@ exports[`should display facet item list with bugs selected 1`] = `
}
/>
}
tooltip="New Bugs"
value="new_bugs"
/>
<span
@@ -249,6 +257,7 @@ exports[`should display facet item list with bugs selected 1`] = `
"leak": "5",
"metric": Object {
"domain": "Reliability",
"id": "1",
"key": "bugs",
"name": "Bugs",
"type": "INT",
@@ -264,6 +273,7 @@ exports[`should display facet item list with bugs selected 1`] = `
}
/>
}
tooltip="Bugs"
value="bugs"
/>
</FacetItemsList>
@@ -300,6 +310,7 @@ exports[`should not display subtitles of new measures if there is none 1`] = `
size={14}
/>
}
tooltip="component_measures.domain_overview"
value="Reliability"
/>
<span
@@ -333,6 +344,7 @@ exports[`should not display subtitles of new measures if there is none 1`] = `
Object {
"metric": Object {
"domain": "Reliability",
"id": "1",
"key": "bugs",
"name": "Bugs",
"type": "INT",
@@ -342,6 +354,7 @@ exports[`should not display subtitles of new measures if there is none 1`] = `
}
/>
}
tooltip="Bugs"
value="bugs"
/>
</FacetItemsList>
@@ -378,6 +391,7 @@ exports[`should not display subtitles of new measures if there is none, even on
size={14}
/>
}
tooltip="component_measures.domain_overview"
value="Reliability"
/>
<span
@@ -411,6 +425,7 @@ exports[`should not display subtitles of new measures if there is none, even on
Object {
"metric": Object {
"domain": "Reliability",
"id": "2",
"key": "new_bugs",
"name": "New Bugs",
"type": "INT",
@@ -420,6 +435,7 @@ exports[`should not display subtitles of new measures if there is none, even on
}
/>
}
tooltip="New Bugs"
value="new_bugs"
/>
</FacetItemsList>

server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap → server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.tsx.snap View File


server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap → server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap View File

@@ -15,6 +15,7 @@ exports[`should display two facets 1`] = `
"leak": "70",
"metric": Object {
"domain": "Coverage",
"id": "1",
"key": "lines_to_cover",
"name": "Lines to Cover",
"type": "INT",
@@ -31,6 +32,7 @@ exports[`should display two facets 1`] = `
"leak": "0.0999999999999943",
"metric": Object {
"domain": "Coverage",
"id": "2",
"key": "coverage",
"name": "Coverage",
"type": "PERCENT",
@@ -61,6 +63,7 @@ exports[`should display two facets 1`] = `
"leak": "0.0",
"metric": Object {
"domain": "Duplications",
"id": "3",
"key": "duplicated_lines_density",
"name": "Duplicated Lines (%)",
"type": "PERCENT",

+ 0
- 58
server/sonar-web/src/main/js/apps/component-measures/types.js View File

@@ -1,58 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 type { Measure, MeasureEnhanced } from '../../components/measure/types'; */

/*:: type ComponentIntern = {
isFavorite?: boolean,
isRecentlyBrowsed?: boolean,
key: string,
match?: string,
name: string,
organization?: string,
project?: string,
qualifier: string
}; */

/*:: export type Component = ComponentIntern & { measures?: Array<Measure> }; */

/*:: export type ComponentEnhanced = ComponentIntern & {
value?: ?string,
leak?: ?string,
measures: Array<MeasureEnhanced>
}; */

/*:: export type Paging = {
pageIndex: number,
pageSize: number,
total: number
}; */

/*:: export type Period = {
index: number,
date: string,
mode: string,
parameter?: string
}; */

/*:: export type Query = {
metric: ?string,
selected: ?string,
view: string
}; */

+ 3
- 6
server/sonar-web/src/main/js/apps/component-measures/utils.ts View File

@@ -79,10 +79,7 @@ export function sortMeasures(
]);
}

export function addMeasureCategories(
domainName: string,
measures: MeasureEnhanced[]
) /*: Array<any> */ {
export function addMeasureCategories(domainName: string, measures: MeasureEnhanced[]) {
const categories = domains[domainName] && domains[domainName].categories;
if (categories && categories.length > 0) {
return [...categories, ...measures];
@@ -121,7 +118,7 @@ export const groupByDomains = memoize((measures: MeasureEnhanced[]) => {
}));

return sortBy(domains, [
(domain: { name: string; measure: MeasureEnhanced[] }) => {
(domain: { name: string; measures: MeasureEnhanced[] }) => {
const idx = KNOWN_DOMAINS.indexOf(domain.name);
return idx >= 0 ? idx : KNOWN_DOMAINS.length;
},
@@ -162,7 +159,7 @@ export function getBubbleMetrics(domain: string, metrics: { [key: string]: Metri
x: metrics[conf.x],
y: metrics[conf.y],
size: metrics[conf.size],
colors: conf.colors ? conf.colors.map(color => metrics[color]) : null
colors: conf.colors && conf.colors.map(color => metrics[color])
};
}


+ 2
- 1
server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx View File

@@ -23,9 +23,10 @@ import DateFromNow from '../../../components/intl/DateFromNow';
import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import Tooltip from '../../../components/controls/Tooltip';
import { getPeriodDate, getPeriodLabel, Period, PeriodMode } from '../../../helpers/periods';
import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods';
import { translateWithParameters } from '../../../helpers/l10n';
import { differenceInDays } from '../../../helpers/dates';
import { Period, PeriodMode } from '../../../app/types';

interface Props {
period: Period;

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

@@ -32,7 +32,7 @@ import { getMeasuresAndMeta } from '../../../api/measures';
import { getAllTimeMachineData, History } from '../../../api/time-machine';
import { parseDate } from '../../../helpers/dates';
import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';
import { getLeakPeriod, Period } from '../../../helpers/periods';
import { getLeakPeriod } from '../../../helpers/periods';
import { get } from '../../../helpers/storage';
import { METRICS, HISTORY_METRICS_LIST } from '../utils';
import {
@@ -48,7 +48,7 @@ import {
} from '../../../helpers/branches';
import { fetchMetrics } from '../../../store/rootActions';
import { getMetrics, Store } from '../../../store/rootReducer';
import { BranchLike, Component, Metric, MeasureEnhanced } from '../../../app/types';
import { BranchLike, Component, Metric, MeasureEnhanced, Period } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
import '../styles.css';


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

@@ -20,8 +20,8 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import LeakPeriodLegend from '../LeakPeriodLegend';
import { PeriodMode, Period } from '../../../../helpers/periods';
import { differenceInDays } from '../../../../helpers/dates';
import { Period, PeriodMode } from '../../../../app/types';

jest.mock('../../../../helpers/dates', () => {
const dates = require.requireActual('../../../../helpers/dates');

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/main/enhance.tsx View File

@@ -33,13 +33,13 @@ import {
getRatingTooltip
} from '../../../helpers/measures';
import { getLocalizedMetricName } from '../../../helpers/l10n';
import { getPeriodDate, Period } from '../../../helpers/periods';
import { getPeriodDate } from '../../../helpers/periods';
import {
getComponentDrilldownUrl,
getComponentIssuesUrl,
getMeasureHistoryUrl
} from '../../../helpers/urls';
import { Component, BranchLike, MeasureEnhanced } from '../../../app/types';
import { Component, BranchLike, MeasureEnhanced, Period } from '../../../app/types';
import { History } from '../../../api/time-machine';
import { getBranchLikeQuery } from '../../../helpers/branches';


+ 1
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx View File

@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* eslint-disable camelcase */
import * as React from 'react';
import { shallow } from 'enzyme';
import ProjectCardLeak from '../ProjectCardLeak';

+ 0
- 1
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx View File

@@ -48,7 +48,6 @@ import {
SourceViewerFile
} from '../../app/types';
import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches';
import { parseDate } from '../../helpers/dates';
import { translate } from '../../helpers/l10n';
import './styles.css';


+ 16
- 12
server/sonar-web/src/main/js/components/charts/BubbleChart.tsx View File

@@ -28,14 +28,16 @@ import { event, select } from 'd3-selection';
import { sortBy, uniq } from 'lodash';
import Tooltip from '../controls/Tooltip';
import { translate } from '../../helpers/l10n';
import { Location } from '../../helpers/urls';
import './BubbleChart.css';

const TICKS_COUNT = 5;

interface BubbleProps {
interface BubbleProps<T> {
color?: string;
link?: string;
onClick?: (link?: string) => void;
link?: string | Location;
onClick?: (ref?: T) => void;
data?: T;
r: number;
scale: number;
tooltip?: string | React.ReactNode;
@@ -43,12 +45,12 @@ interface BubbleProps {
y: number;
}

export class Bubble extends React.PureComponent<BubbleProps> {
export class Bubble<T> extends React.PureComponent<BubbleProps<T>> {
handleClick = (event: React.MouseEvent<SVGCircleElement>) => {
if (this.props.onClick) {
event.stopPropagation();
event.preventDefault();
this.props.onClick(this.props.link);
this.props.onClick(this.props.data);
}
};

@@ -75,17 +77,18 @@ export class Bubble extends React.PureComponent<BubbleProps> {
}
}

interface Item {
export interface BubbleItem<T> {
color?: string;
key?: string;
link?: any;
link?: string | Location;
data?: T;
size: number;
tooltip?: React.ReactNode;
x: number;
y: number;
}

interface Props {
interface Props<T> {
displayXGrid?: boolean;
displayXTicks?: boolean;
displayYGrid?: boolean;
@@ -93,8 +96,8 @@ interface Props {
formatXTick?: (tick: number) => string;
formatYTick?: (tick: number) => string;
height: number;
items: Item[];
onBubbleClick?: (link?: string) => void;
items: BubbleItem<T>[];
onBubbleClick?: (ref?: T) => void;
padding?: [number, number, number, number];
sizeDomain?: [number, number];
sizeRange?: [number, number];
@@ -108,7 +111,7 @@ interface State {

type Scale = ScaleLinear<number, number>;

export default class BubbleChart extends React.Component<Props, State> {
export default class BubbleChart<T> extends React.Component<Props<T>, State> {
node: SVGSVGElement | null = null;
selection: any = null;
transform: any = null;
@@ -122,7 +125,7 @@ export default class BubbleChart extends React.Component<Props, State> {
sizeRange: [5, 45]
};

constructor(props: Props) {
constructor(props: Props<T>) {
super(props);
this.state = { transform: { x: 0, y: 0, k: 1 } };
}
@@ -317,6 +320,7 @@ export default class BubbleChart extends React.Component<Props, State> {
key={item.key || index}
link={item.link}
onClick={this.props.onBubbleClick}
data={item.data}
r={sizeScale(item.size)}
scale={1 / transform.k}
tooltip={item.tooltip}

+ 1
- 1
server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx View File

@@ -35,7 +35,7 @@ it('should render bubble links', () => {

it('should render bubbles with click handlers', () => {
const onClick = jest.fn();
const items = [{ x: 1, y: 10, size: 7, link: 'foo' }, { x: 2, y: 30, size: 5, link: 'bar' }];
const items = [{ x: 1, y: 10, size: 7, data: 'foo' }, { x: 2, y: 30, size: 5, data: 'bar' }];
const chart = mount(<BubbleChart height={100} items={items} onBubbleClick={onClick} />);
chart.find(Bubble).forEach(bubble => expect(bubble).toMatchSnapshot());
});

+ 2
- 2
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap View File

@@ -130,8 +130,8 @@ exports[`should render bubble links 2`] = `

exports[`should render bubbles with click handlers 1`] = `
<Bubble
data="foo"
key="0"
link="foo"
onClick={[MockFunction]}
r={45}
scale={1}
@@ -159,8 +159,8 @@ exports[`should render bubbles with click handlers 1`] = `

exports[`should render bubbles with click handlers 2`] = `
<Bubble
data="bar"
key="1"
link="bar"
onClick={[MockFunction]}
r={33.57142857142857}
scale={1}

+ 2
- 1
server/sonar-web/src/main/js/components/measure/utils.ts View File

@@ -19,6 +19,7 @@
*/
import { getRatingTooltip as nextGetRatingTooltip, isDiffMetric } from '../../helpers/measures';
import { Metric, Measure, MeasureEnhanced } from '../../app/types';
import { getLeakPeriod } from '../../helpers/periods';

const KNOWN_RATINGS = ['sqale_rating', 'reliability_rating', 'security_rating'];

@@ -37,7 +38,7 @@ export function getLeakValue(measure: Measure | undefined): string | undefined {
if (!measure || !measure.periods) {
return undefined;
}
const period = measure.periods.find(period => period.index === 1);
const period = getLeakPeriod(measure.periods);
return period && period.value;
}


+ 6
- 10
server/sonar-web/src/main/js/helpers/path.ts View File

@@ -87,16 +87,12 @@ export function fileFromPath(path: string | null): string | null {
}
}

export function splitPath(path: string): { head: string; tail: string } | null {
if (typeof path === 'string') {
const tokens = path.split('/');
return {
head: tokens.slice(0, -1).join('/'),
tail: tokens[tokens.length - 1]
};
} else {
return null;
}
export function splitPath(path: string) {
const tokens = path.split('/');
return {
head: tokens.slice(0, -1).join('/'),
tail: tokens[tokens.length - 1]
};
}

export function limitComponentName(str: string, limit = 30): string {

+ 3
- 19
server/sonar-web/src/main/js/helpers/periods.ts View File

@@ -19,32 +19,16 @@
*/
import { translate, translateWithParameters } from './l10n';
import { parseDate } from './dates';
import { Period, PeriodMode, PeriodMeasure } from '../app/types';

export enum PeriodMode {
Days = 'days',
Date = 'date',
Version = 'version',
PreviousAnalysis = 'previous_analysis',
PreviousVersion = 'previous_version'
}

export interface Period {
date: string;
index: number;
mode: PeriodMode;
modeParam?: string;
parameter?: string;
}

function getPeriod(periods: Period[] | undefined, index: number) {
function getPeriod<T extends Period | PeriodMeasure>(periods: T[] | undefined, index: number) {
if (!Array.isArray(periods)) {
return undefined;
}

return periods.find(period => period.index === index);
}

export function getLeakPeriod(periods: Period[] | undefined) {
export function getLeakPeriod<T extends Period | PeriodMeasure>(periods: T[] | undefined) {
return getPeriod(periods, 1);
}


Loading…
Cancel
Save