From 81620b85dcbb883166e9d1e13d8cc0207ca61a80 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Wed, 15 Aug 2018 11:03:24 +0200 Subject: [PATCH] SONAR-11140 Rewrite part of component measures page to TS --- .../sonar-web/src/main/js/api/components.ts | 15 +- server/sonar-web/src/main/js/app/types.ts | 27 ++++ ...{utils-test.js.snap => utils-test.ts.snap} | 7 + .../{utils-test.js => utils-test.ts} | 22 ++- .../{Breadcrumb.js => Breadcrumb.tsx} | 27 ++-- .../{Breadcrumbs.js => Breadcrumbs.tsx} | 45 +++--- .../{MeasureContent.js => MeasureContent.tsx} | 134 ++++++++---------- ...ureViewSelect.js => MeasureViewSelect.tsx} | 72 +++++----- ...Breadcrumb-test.js => Breadcrumb-test.tsx} | 4 +- ...eadcrumbs-test.js => Breadcrumbs-test.tsx} | 56 +++++--- ...ect-test.js => MeasureViewSelect-test.tsx} | 9 +- ...-test.js.snap => Breadcrumb-test.tsx.snap} | 0 .../__snapshots__/Breadcrumbs-test.js.snap | 93 ------------ .../__snapshots__/Breadcrumbs-test.tsx.snap | 37 +++++ ...s.snap => MeasureViewSelect-test.tsx.snap} | 22 +-- .../config/{bubbles.js => bubbles.ts} | 12 +- .../{complementary.js => complementary.ts} | 4 +- .../config/{domains.js => domains.ts} | 3 +- .../drilldown/{CodeView.js => CodeView.tsx} | 33 ++--- .../{ComponentsList.js => ComponentsList.tsx} | 100 ++++++------- .../{EmptyResult.js => EmptyResult.tsx} | 3 +- .../drilldown/{FilesView.js => FilesView.tsx} | 63 ++++---- .../{TreeMapView.js => TreeMapView.tsx} | 112 ++++++++------- .../component-measures/{utils.js => utils.ts} | 87 ++++++------ .../components/charts/ColorGradientLegend.tsx | 2 +- .../src/main/js/components/charts/TreeMap.tsx | 4 +- .../main/js/components/charts/TreeMapRect.tsx | 2 +- .../sonar-web/src/main/js/helpers/measures.ts | 5 +- 28 files changed, 494 insertions(+), 506 deletions(-) rename server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/{utils-test.js.snap => utils-test.ts.snap} (93%) rename server/sonar-web/src/main/js/apps/component-measures/__tests__/{utils-test.js => utils-test.ts} (84%) rename server/sonar-web/src/main/js/apps/component-measures/components/{Breadcrumb.js => Breadcrumb.tsx} (80%) rename server/sonar-web/src/main/js/apps/component-measures/components/{Breadcrumbs.js => Breadcrumbs.tsx} (79%) rename server/sonar-web/src/main/js/apps/component-measures/components/{MeasureContent.js => MeasureContent.tsx} (78%) rename server/sonar-web/src/main/js/apps/component-measures/components/{MeasureViewSelect.js => MeasureViewSelect.tsx} (63%) rename server/sonar-web/src/main/js/apps/component-measures/components/__tests__/{Breadcrumb-test.js => Breadcrumb-test.tsx} (94%) rename server/sonar-web/src/main/js/apps/component-measures/components/__tests__/{Breadcrumbs-test.js => Breadcrumbs-test.tsx} (63%) rename server/sonar-web/src/main/js/apps/component-measures/components/__tests__/{MeasureViewSelect-test.js => MeasureViewSelect-test.tsx} (82%) rename server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/{Breadcrumb-test.js.snap => Breadcrumb-test.tsx.snap} (100%) delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.tsx.snap rename server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/{MeasureViewSelect-test.js.snap => MeasureViewSelect-test.tsx.snap} (52%) rename server/sonar-web/src/main/js/apps/component-measures/config/{bubbles.js => bubbles.ts} (89%) rename server/sonar-web/src/main/js/apps/component-measures/config/{complementary.js => complementary.ts} (94%) rename server/sonar-web/src/main/js/apps/component-measures/config/{domains.js => domains.ts} (97%) rename server/sonar-web/src/main/js/apps/component-measures/drilldown/{CodeView.js => CodeView.tsx} (76%) rename server/sonar-web/src/main/js/apps/component-measures/drilldown/{ComponentsList.js => ComponentsList.tsx} (61%) rename server/sonar-web/src/main/js/apps/component-measures/drilldown/{EmptyResult.js => EmptyResult.tsx} (96%) rename server/sonar-web/src/main/js/apps/component-measures/drilldown/{FilesView.js => FilesView.tsx} (73%) rename server/sonar-web/src/main/js/apps/component-measures/drilldown/{TreeMapView.js => TreeMapView.tsx} (72%) rename server/sonar-web/src/main/js/apps/component-measures/{utils.js => utils.ts} (66%) diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index f6e83e322b9..23b90e37a33 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -19,7 +19,14 @@ */ import throwGlobalError from '../app/utils/throwGlobalError'; import { getJSON, postJSON, post, RequestData } from '../helpers/request'; -import { Paging, Visibility, BranchParameters, MyProject } from '../app/types'; +import { + Paging, + Visibility, + BranchParameters, + MyProject, + Metric, + ComponentMeasure +} from '../app/types'; export interface BaseSearchProjectsParameters { analyzedBefore?: string; @@ -93,7 +100,11 @@ export function getComponentTree( componentKey: string, metrics: string[] = [], additional: RequestData = {} -): Promise { +): Promise<{ + components: ComponentMeasure[]; + metrics: Metric[]; + paging: Paging; +}> { const url = '/api/measures/component_tree'; const data = Object.assign({}, additional, { baseComponentKey: componentKey, diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index f081619542f..8a1da4194d6 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -1,3 +1,5 @@ +import { Measure, MeasureEnhanced } from '../helpers/measures'; + /* * SonarQube * Copyright (C) 2009-2018 SonarSource SA @@ -340,18 +342,43 @@ export interface MainBranch extends Branch { status?: { qualityGateStatus: string }; } +interface ComponentMeasureIntern { + isFavorite?: boolean; + isRecentlyBrowsed?: boolean; + key: string; + match?: string; + name: string; + organization?: string; + project?: string; + qualifier: string; + refKey?: string; +} + +export interface ComponentMeasure extends ComponentMeasureIntern { + measures?: Measure[]; +} + +export interface ComponentMeasureEnhanced extends ComponentMeasureIntern { + value?: string; + leak?: string; + measures: MeasureEnhanced[]; +} + export interface Metric { + bestValue?: string; custom?: boolean; decimalScale?: number; description?: string; direction?: number; domain?: string; hidden?: boolean; + higherValuesAreBetter?: boolean; id: string; key: string; name: string; qualitative?: boolean; type: string; + worstValue?: string; } export interface MyProject { diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.ts.snap similarity index 93% rename from server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.js.snap rename to server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.ts.snap index 979e672595d..4493d5a8038 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.ts.snap @@ -8,6 +8,7 @@ Array [ "leak": "70", "metric": Object { "domain": "Coverage", + "id": "1", "key": "lines_to_cover", "name": "Lines to Cover", "type": "INT", @@ -24,6 +25,7 @@ Array [ "leak": "0.0999999999999943", "metric": Object { "domain": "Coverage", + "id": "2", "key": "coverage", "name": "Coverage", "type": "PERCENT", @@ -45,6 +47,7 @@ Array [ "leak": "0.0", "metric": Object { "domain": "Duplications", + "id": "3", "key": "duplicated_lines_density", "name": "Duplicated Lines (%)", "type": "PERCENT", @@ -67,6 +70,7 @@ exports[`sortMeasures should sort based on the config 1`] = ` Array [ Object { "metric": Object { + "id": "3", "key": "new_bugs", "name": "new_bugs", "type": "INT", @@ -74,6 +78,7 @@ Array [ }, Object { "metric": Object { + "id": "2", "key": "new_reliability_remediation_effort", "name": "bugs", "type": "INT", @@ -82,6 +87,7 @@ Array [ "overall_category", Object { "metric": Object { + "id": "4", "key": "bugs", "name": "bugs", "type": "INT", @@ -89,6 +95,7 @@ Array [ }, Object { "metric": Object { + "id": "1", "key": "reliability_remediation_effort", "name": "new_bugs", "type": "INT", diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts similarity index 84% rename from server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js rename to server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts index 767208c79c5..74987c7eaa5 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts @@ -17,12 +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. */ -// @flow import * as utils from '../utils'; const MEASURES = [ { metric: { + id: '1', key: 'lines_to_cover', type: 'INT', name: 'Lines to Cover', @@ -34,6 +34,7 @@ const MEASURES = [ }, { metric: { + id: '2', key: 'coverage', type: 'PERCENT', name: 'Coverage', @@ -45,6 +46,7 @@ const MEASURES = [ }, { metric: { + id: '3', key: 'duplicated_lines_density', type: 'PERCENT', name: 'Duplicated Lines (%)', @@ -60,8 +62,10 @@ describe('filterMeasures', () => { it('should exclude banned measures', () => { expect( utils.filterMeasures([ - { metric: { key: 'bugs', name: 'Bugs', type: 'INT' } }, - { metric: { key: 'critical_violations', name: 'Critical Violations', type: 'INT' } } + { metric: { id: '1', key: 'bugs', name: 'Bugs', type: 'INT' } }, + { + metric: { id: '2', key: 'critical_violations', name: 'Critical Violations', type: 'INT' } + } ]) ).toHaveLength(1); }); @@ -71,10 +75,14 @@ describe('sortMeasures', () => { it('should sort based on the config', () => { expect( utils.sortMeasures('Reliability', [ - { metric: { key: 'reliability_remediation_effort', name: 'new_bugs', type: 'INT' } }, - { metric: { key: 'new_reliability_remediation_effort', name: 'bugs', type: 'INT' } }, - { metric: { key: 'new_bugs', name: 'new_bugs', type: 'INT' } }, - { metric: { key: 'bugs', name: 'bugs', type: 'INT' } }, + { + metric: { id: '1', key: 'reliability_remediation_effort', name: 'new_bugs', type: 'INT' } + }, + { + metric: { id: '2', key: 'new_reliability_remediation_effort', name: 'bugs', type: 'INT' } + }, + { metric: { id: '3', key: 'new_bugs', name: 'new_bugs', type: 'INT' } }, + { metric: { id: '4', key: 'bugs', name: 'bugs', type: 'INT' } }, 'overall_category' ]) ).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx similarity index 80% rename from server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.js rename to server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx index 78200b351b1..326401a8377 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx @@ -17,25 +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 Tooltip from '../../../components/controls/Tooltip'; import { collapsePath, limitComponentName } from '../../../helpers/path'; -/*:: import type { Component } from '../types'; */ +import { ComponentMeasure } from '../../../app/types'; -/*:: type Props = { - canBrowse: boolean, - component: Component, - isLast: boolean, - handleSelect: string => void -}; */ - -export default class Breadcrumb extends React.PureComponent { - /*:: props: Props; */ +interface Props { + canBrowse: boolean; + component: ComponentMeasure; + isLast: boolean; + handleSelect: (component: string) => void; +} - handleClick = (e /*: Event & { target: HTMLElement } */) => { - e.preventDefault(); - e.target.blur(); +export default class Breadcrumb extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); this.props.handleSelect(this.props.component.key); }; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx similarity index 79% rename from server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js rename to server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx index fbc87441a60..f90d46e7550 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx @@ -17,33 +17,29 @@ * 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 key from 'keymaster'; +import * as React from 'react'; +import * as key from 'keymaster'; import Breadcrumb from './Breadcrumb'; import { getBreadcrumbs } from '../../../api/components'; import { getBranchLikeQuery } from '../../../helpers/branches'; -/*:: import type { Component } from '../types'; */ +import { BranchLike, ComponentMeasure } from '../../../app/types'; -/*:: type Props = {| - backToFirst: boolean, - branchLike?: { id?: string, name: string }, - className?: string, - component: Component, - handleSelect: string => void, - rootComponent: Component -|}; */ +interface Props { + backToFirst: boolean; + branchLike?: BranchLike; + className?: string; + component: ComponentMeasure; + handleSelect: (component: string) => void; + rootComponent: ComponentMeasure; +} -/*:: type State = { - breadcrumbs: Array -}; */ +interface State { + breadcrumbs: ComponentMeasure[]; +} -export default class Breadcrumbs extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: props: Props; */ - state /*: State */ = { - breadcrumbs: [] - }; +export default class Breadcrumbs extends React.PureComponent { + mounted = false; + state: State = { breadcrumbs: [] }; componentDidMount() { this.mounted = true; @@ -51,7 +47,7 @@ export default class Breadcrumbs extends React.PureComponent { this.attachShortcuts(); } - componentWillReceiveProps(nextProps /*: Props */) { + componentWillReceiveProps(nextProps: Props) { if (this.props.component !== nextProps.component) { this.fetchBreadcrumbs(nextProps); } @@ -77,7 +73,7 @@ export default class Breadcrumbs extends React.PureComponent { key.unbind('left', 'measures-files'); } - fetchBreadcrumbs = ({ branchLike, component, rootComponent } /*: Props */) => { + fetchBreadcrumbs = ({ branchLike, component, rootComponent }: Props) => { const isRoot = component.key === rootComponent.key; if (isRoot) { if (this.mounted) { @@ -90,7 +86,8 @@ export default class Breadcrumbs extends React.PureComponent { if (this.mounted) { this.setState({ breadcrumbs }); } - } + }, + () => {} ); }; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx similarity index 78% rename from server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js rename to server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx index 8777d0fca7e..6bdd4d6f190 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx @@ -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 React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; +import { InjectedRouter } from 'react-router'; import Breadcrumbs from './Breadcrumbs'; import MeasureFavoriteContainer from './MeasureFavoriteContainer'; import MeasureHeader from './MeasureHeader'; @@ -33,62 +33,58 @@ import { getComponentTree } from '../../../api/components'; import { complementary } from '../config/complementary'; import { enhanceComponent, isFileType, isViewType } from '../utils'; import { getProjectUrl } from '../../../helpers/urls'; -import { isDiffMetric } from '../../../helpers/measures'; +import { isDiffMetric, MeasureEnhanced } from '../../../helpers/measures'; import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; -/*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */ -/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ -/*:: import type { Metric } from '../../../store/metrics/actions'; */ +import { + ComponentMeasure, + ComponentMeasureEnhanced, + BranchLike, + Metric, + Paging +} from '../../../app/types'; +import { RequestData } from '../../../helpers/request'; +import { Period } from '../../../helpers/periods'; -// Switching to the following type will make flow crash with : -// https://github.com/facebook/flow/issues/3147 -// router: { push: ({ pathname: string, query?: RawQuery }) => void } -/*:: type Props = {| - branchLike?: { id?: string; name: string }, - className?: string, - component: Component, - currentUser: { isLoggedIn: boolean }, - loading: boolean, - leakPeriod?: Period, - measure: ?MeasureEnhanced, - metric: Metric, - metrics: { [string]: Metric }, - rootComponent: Component, - router: Object, - secondaryMeasure: ?MeasureEnhanced, - updateLoading: ({ [string]: boolean }) => void, - updateSelected: string => void, - updateView: string => void, - view: string -|}; */ +interface Props { + branchLike?: BranchLike; + className?: string; + component: ComponentMeasure; + currentUser: { isLoggedIn: boolean }; + loading: boolean; + leakPeriod?: Period; + measure?: MeasureEnhanced; + metric: Metric; + metrics: { [metric: string]: Metric }; + rootComponent: ComponentMeasure; + router: InjectedRouter; + secondaryMeasure?: MeasureEnhanced; + updateLoading: (param: { [key: string]: boolean }) => void; + updateSelected: (component: string) => void; + updateView: (view: string) => void; + view: string; +} -/*:: type State = { - bestValue?: string, - components: Array, - metric: ?Metric, - paging?: Paging, - selected: ?string, - view: ?string -}; */ +interface State { + bestValue?: string; + components: ComponentMeasureEnhanced[]; + metric?: Metric; + paging?: Paging; + selected?: string; + view?: string; +} -export default class MeasureContent extends React.PureComponent { - /*:: container: HTMLElement; */ - /*:: mounted: boolean; */ - /*:: props: Props; */ - state /*: State */ = { - components: [], - metric: null, - paging: null, - selected: null, - view: null - }; +export default class MeasureContent extends React.PureComponent { + container?: HTMLElement | null; + mounted = false; + state: State = { components: [] }; componentDidMount() { this.mounted = true; this.fetchComponents(this.props); } - componentWillReceiveProps(nextProps /*: Props */) { + componentWillReceiveProps(nextProps: Props) { if ( !isSameBranchLike(nextProps.branchLike, this.props.branchLike) || nextProps.component !== this.props.component || @@ -107,17 +103,13 @@ export default class MeasureContent extends React.PureComponent { ? this.props.component.key : this.state.selected; const index = this.state.components.findIndex(component => component.key === componentKey); - return index !== -1 ? index : null; + return index !== -1 ? index : undefined; }; - getComponentRequestParams = ( - view /*: string */, - metric /*: Metric */, - options /*: Object */ = {} - ) => { + getComponentRequestParams = (view: string, metric: Metric, options: Object = {}) => { const strategy = view === 'list' ? 'leaves' : 'children'; const metricKeys = [metric.key]; - const opts /*: Object */ = { + const opts: RequestData = { ...getBranchLikeQuery(this.props.branchLike), additionalFields: 'metrics', metricSortFilter: 'withMeasuresOnly' @@ -142,9 +134,10 @@ export default class MeasureContent extends React.PureComponent { return { metricKeys, opts: { ...opts, ...options }, strategy }; }; - fetchComponents = ({ component, metric, metrics, view } /*: Props */) => { + fetchComponents = ({ component, metric, metrics, view }: Props) => { if (isFileType(component)) { - return this.setState({ metric: null, view: null }); + this.setState({ metric: undefined, view: undefined }); + return; } const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric); @@ -153,7 +146,7 @@ export default class MeasureContent extends React.PureComponent { r => { if (metric === this.props.metric) { if (this.mounted) { - this.setState(({ selected } /*: State */) => ({ + this.setState(({ selected }: State) => ({ bestValue: r.metrics[0].bestValue, components: r.components.map(component => enhanceComponent(component, metric, metrics) @@ -206,12 +199,12 @@ export default class MeasureContent extends React.PureComponent { ); }; - onOpenComponent = (componentKey /*: string */) => { + onOpenComponent = (componentKey: string) => { if (isViewType(this.props.rootComponent)) { const component = this.state.components.find( component => component.refKey === componentKey || component.key === componentKey ); - if (component && component.refKey != null) { + if (component && component.refKey !== undefined) { if (this.props.view === 'treemap') { this.props.router.push(getProjectUrl(componentKey)); } @@ -225,7 +218,7 @@ export default class MeasureContent extends React.PureComponent { } }; - onSelectComponent = (componentKey /*: string */) => this.setState({ selected: componentKey }); + onSelectComponent = (componentKey: string) => this.setState({ selected: componentKey }); renderCode() { return ( @@ -245,8 +238,8 @@ export default class MeasureContent extends React.PureComponent { renderMeasure() { const { metric, view } = this.state; - if (metric != null) { - if (['list', 'tree'].includes(view)) { + if (metric !== undefined) { + if (!view || ['list', 'tree'].includes(view)) { const selectedIdx = this.getSelectedIndex(); return ( ); } @@ -289,8 +282,7 @@ export default class MeasureContent extends React.PureComponent { return (
(this.container = container)} - tabIndex={0}> + ref={container => (this.container = container)}>
@@ -319,7 +311,9 @@ export default class MeasureContent extends React.PureComponent { /> )}
- {metric == null && ( - - )} - {metric != null && ( + {!metric && } + {metric && (
void, - view: string -}; */ - -export default class MeasureViewSelect extends React.PureComponent { - /*:: props: Props; */ +interface Props { + className?: string; + metric: Metric; + handleViewChange: (view: string) => void; + view: string; +} +export default class MeasureViewSelect extends React.PureComponent { getOptions = () => { const { metric } = this.props; const options = []; if (hasList(metric.key)) { options.push({ - value: 'list', - label: ( -
- - {translate('component_measures.tab.list')} -
- ), - icon: + icon: , + label: translate('component_measures.tab.list'), + value: 'list' }); } if (hasTree(metric.key)) { options.push({ - value: 'tree', - label: ( -
- - {translate('component_measures.tab.tree')} -
- ), - icon: + icon: , + label: translate('component_measures.tab.tree'), + value: 'tree' }); } if (hasTreemap(metric.key, metric.type)) { options.push({ - value: 'treemap', - label: ( -
- - {translate('component_measures.tab.treemap')} -
- ), - icon: + icon: , + label: translate('component_measures.tab.treemap'), + value: 'treemap' }); } return options; }; - handleChange = (option /*: { value: string } */) => this.props.handleViewChange(option.value); + handleChange = (option: { value: string }) => { + return this.props.handleViewChange(option.value); + }; + + renderOption = (option: { icon: JSX.Element; label: string }) => { + return ( + <> + {option.icon} + {option.label} + + ); + }; - renderValue = (value /*: { icon: Element<*> } */) => value.icon; + renderValue = (value: { icon: JSX.Element }) => { + return value.icon; + }; render() { return ( @@ -90,6 +85,7 @@ export default class MeasureViewSelect extends React.PureComponent { className={this.props.className} clearable={false} onChange={this.handleChange} + optionRenderer={this.renderOption} options={this.getOptions()} searchable={false} value={this.props.view} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.tsx similarity index 94% rename from server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.js rename to server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.tsx index cd4c0298fb8..8eff874efdd 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.tsx @@ -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 Breadcrumb from '../Breadcrumb'; @@ -29,6 +29,7 @@ it('should show the last element without clickable link', () => { component={{ key: 'foo', name: 'Foo', + organization: 'foo', qualifier: 'TRK' }} handleSelect={() => {}} @@ -46,6 +47,7 @@ it('should correctly show a middle element', () => { component={{ key: 'foo', name: 'Foo', + organization: 'foo', qualifier: 'TRK' }} handleSelect={() => {}} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.tsx similarity index 63% rename from server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js rename to server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.tsx index a011498c1dc..3511f452b1f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.tsx @@ -17,28 +17,47 @@ * 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 { mount } from 'enzyme'; import Breadcrumbs from '../Breadcrumbs'; -import { doAsync } from '../../../../helpers/testUtils'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { getBreadcrumbs } from '../../../../api/components'; jest.mock('../../../../api/components', () => ({ - getBreadcrumbs: () => - Promise.resolve([ + getBreadcrumbs: jest + .fn() + .mockResolvedValue([ { key: 'anc1', name: 'Ancestor1' }, { key: 'anc2', name: 'Ancestor2' }, { key: 'bar', name: 'Bar' } ]) })); +const componentFoo = { + key: 'foo', + name: 'Foo', + organization: 'bar', + qualifier: 'TRK' +}; + +const componentBar = { + key: 'bar', + name: 'Bar', + organization: 'bar', + qualifier: 'TRK' +}; + +beforeEach(() => { + (getBreadcrumbs as jest.Mock).mockClear(); +}); + it('should display correctly for the list view', () => { const wrapper = mount( {}} - rootComponent={{ key: 'foo', name: 'Foo' }} - view="list" + rootComponent={componentFoo} /> ); expect(wrapper).toMatchSnapshot(); @@ -47,27 +66,24 @@ it('should display correctly for the list view', () => { it('should display only the root component', () => { const wrapper = mount( {}} - rootComponent={{ key: 'foo', name: 'Foo' }} - view="tree" + rootComponent={componentFoo} /> ); expect(wrapper.state()).toMatchSnapshot(); }); -it.only('should load the breadcrumb from the api', () => { +it('should load the breadcrumb from the api', async () => { const wrapper = mount( {}} - rootComponent={{ key: 'foo', name: 'Foo' }} - view="tree" + rootComponent={componentFoo} /> ); - return doAsync(() => { - expect(wrapper.state()).toMatchSnapshot(); - }); + await waitAndUpdate(wrapper); + expect(getBreadcrumbs).toHaveBeenCalled(); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.tsx similarity index 82% rename from server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.js rename to server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.tsx index 48b39433226..a54ea14a3a1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.tsx @@ -17,14 +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 MeasureViewSelect from '../MeasureViewSelect'; +import { Metric } from '../../../../app/types'; it('should display correctly with treemap option', () => { expect( shallow( - {}} metric={{ type: 'PERCENT' }} view="tree" /> + {}} + metric={{ type: 'PERCENT' } as Metric} + view="tree" + /> ) ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.tsx.snap similarity index 100% rename from server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.js.snap rename to server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap deleted file mode 100644 index 63bab85ba77..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap +++ /dev/null @@ -1,93 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display correctly for the list view 1`] = ` - -
- - - - Foo - - - - - - - - Bar - - - -
-
-`; - -exports[`should display only the root component 1`] = ` -Object { - "breadcrumbs": Array [ - Object { - "key": "foo", - "name": "Foo", - }, - ], -} -`; - -exports[`should load the breadcrumb from the api 1`] = ` -Object { - "breadcrumbs": Array [ - Object { - "key": "anc1", - "name": "Ancestor1", - }, - Object { - "key": "anc2", - "name": "Ancestor2", - }, - Object { - "key": "bar", - "name": "Bar", - }, - ], -} -`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.tsx.snap new file mode 100644 index 00000000000..9e4fbb301a8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display correctly for the list view 1`] = ` + +`; + +exports[`should display only the root component 1`] = ` +Object { + "breadcrumbs": Array [ + Object { + "key": "foo", + "name": "Foo", + "organization": "bar", + "qualifier": "TRK", + }, + ], +} +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.tsx.snap similarity index 52% rename from server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap rename to server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.tsx.snap index 6a46b06820b..43cd04e0644 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.tsx.snap @@ -5,36 +5,22 @@ exports[`should display correctly with treemap option 1`] = ` autoBlur={true} clearable={false} onChange={[Function]} + optionRenderer={[Function]} options={ Array [ Object { "icon": , - "label":
- - component_measures.tab.list -
, + "label": "component_measures.tab.list", "value": "list", }, Object { "icon": , - "label":
- - component_measures.tab.tree -
, + "label": "component_measures.tab.tree", "value": "tree", }, Object { "icon": , - "label":
- - component_measures.tab.treemap -
, + "label": "component_measures.tab.treemap", "value": "treemap", }, ] diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts similarity index 89% rename from server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js rename to server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts index d00c66bf9f3..d59bb1d91e1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js +++ b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.ts @@ -17,8 +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 -export const bubbles = { +export const bubbles: { + [domain: string]: { + x: string; + y: string; + size: string; + colors?: string[]; + yDomain?: number[]; + }; +} = { Reliability: { x: 'ncloc', y: 'reliability_remediation_effort', @@ -39,6 +46,7 @@ export const bubbles = { }, Coverage: { x: 'complexity', y: 'coverage', size: 'uncovered_lines', yDomain: [100, 0] }, Duplications: { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' }, + // eslint-disable-next-line project_overview: { x: 'sqale_index', y: 'coverage', diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/complementary.js b/server/sonar-web/src/main/js/apps/component-measures/config/complementary.ts similarity index 94% rename from server/sonar-web/src/main/js/apps/component-measures/config/complementary.js rename to server/sonar-web/src/main/js/apps/component-measures/config/complementary.ts index c67a3de7d2d..9c9824c6a4b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/config/complementary.js +++ b/server/sonar-web/src/main/js/apps/component-measures/config/complementary.ts @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -export const complementary = { +/* eslint-disable camelcase */ +export const complementary: { [metric: string]: string[] } = { coverage: ['uncovered_lines', 'uncovered_conditions'], line_coverage: ['uncovered_lines'], branch_coverage: ['uncovered_conditions'], diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/domains.js b/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts similarity index 97% rename from server/sonar-web/src/main/js/apps/component-measures/config/domains.js rename to server/sonar-web/src/main/js/apps/component-measures/config/domains.ts index 8a746deb636..d7022b680b4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/config/domains.js +++ b/server/sonar-web/src/main/js/apps/component-measures/config/domains.ts @@ -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 -export const domains /*: { [string]: { categories?: Array, order: Array } }*/ = { +export const domains: { [domain: string]: { categories?: string[]; order: string[] } } = { Reliability: { categories: ['new_code_category', 'overall_category'], order: [ diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.tsx similarity index 76% rename from server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js rename to server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.tsx index 1c0747851f1..6f74390695b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.tsx @@ -17,26 +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 key from 'keymaster'; +import * as React from 'react'; +import * as key from 'keymaster'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; -/*:: import type { ComponentEnhanced, Paging, Period } from '../types'; */ -/*:: import type { Metric } from '../../../store/metrics/actions'; */ +import { BranchLike, ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../app/types'; +import { Period } from '../../../helpers/periods'; -/*:: type Props = {| - branchLike?: { id?: string; name: string }, - component: ComponentEnhanced, - components: Array, - leakPeriod?: Period, - metric: Metric, - selectedIdx: ?number, - updateSelected: string => void, -|}; */ - -export default class CodeView extends React.PureComponent { - /*:: props: Props; */ +interface Props { + branchLike?: BranchLike; + component: ComponentMeasure; + components: Array; + leakPeriod?: Period; + metric: Metric; + selectedIdx?: number; + updateSelected: (component: string) => void; +} +export default class CodeView extends React.PureComponent { componentDidMount() { this.attachShortcuts(); } @@ -57,7 +54,7 @@ export default class CodeView extends React.PureComponent { } detachShortcuts() { - ['j', 'k'].map(action => key.unbind(action, 'measures-files')); + ['j', 'k'].forEach(action => key.unbind(action, 'measures-files')); } selectPrevious = () => { diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx similarity index 61% rename from server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js rename to server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx index fbd26322b63..ec49ffb6e9c 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx @@ -17,74 +17,53 @@ * 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 ComponentsListRow from './ComponentsListRow'; import EmptyResult from './EmptyResult'; import { complementary } from '../config/complementary'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric, isPeriodBestValue } from '../../../helpers/measures'; -/*:: import type { Component, ComponentEnhanced } from '../types'; */ -/*:: import type { Metric } from '../../../store/metrics/actions'; */ +import { ComponentMeasure, ComponentMeasureEnhanced, Metric, BranchLike } from '../../../app/types'; +import { Button } from '../../../components/ui/buttons'; -/*:: type Props = {| - bestValue?: string, - branchLike?: { id?: string; name: string }, - components: Array, - onClick: string => void, - metric: Metric, - metrics: { [string]: Metric }, - rootComponent: Component, - selectedComponent?: ?string -|}; */ +interface Props { + bestValue?: string; + branchLike?: BranchLike; + components: ComponentMeasureEnhanced[]; + onClick: (component: string) => void; + metric: Metric; + metrics: { [metric: string]: Metric }; + rootComponent: ComponentMeasure; + selectedComponent?: string; +} -/*:: type State = { - hideBest: boolean -}; */ +interface State { + hideBest: boolean; +} -export default class ComponentsList extends React.PureComponent { - /*:: props: Props; */ - state /*: State */ = { - hideBest: true - }; +export default class ComponentsList extends React.PureComponent { + state: State = { hideBest: true }; - componentWillReceiveProps(nextProps /*: Props */) { + componentWillReceiveProps(nextProps: Props) { if (nextProps.metric !== this.props.metric) { this.setState({ hideBest: true }); } } - displayAll = (event /*: Event */) => { - event.preventDefault(); + displayAll = () => { this.setState({ hideBest: false }); }; - hasBestValue(component /*: Component*/, otherMetrics /*: Array */) { + hasBestValue = (component: ComponentMeasureEnhanced) => { const { metric } = this.props; const focusedMeasure = component.measures.find(measure => measure.metric.key === metric.key); - if (isDiffMetric(focusedMeasure.metric.key)) { + if (focusedMeasure && isDiffMetric(focusedMeasure.metric.key)) { return isPeriodBestValue(focusedMeasure, 1); } - return focusedMeasure.bestValue; - } - - renderComponent(component /*: Component*/, otherMetrics /*: Array */) { - const { branchLike, metric, selectedComponent, onClick, rootComponent } = this.props; - return ( - - ); - } + return Boolean(focusedMeasure && focusedMeasure.bestValue); + }; - renderHiddenLink(hiddenCount /*: number*/, colCount /*: number*/) { + renderHiddenLink = (hiddenCount: number) => { return (
{translateWithParameters( @@ -92,12 +71,12 @@ export default class ComponentsList extends React.PureComponent { hiddenCount, formatMeasure(this.props.bestValue, this.props.metric.type) )} - +
); - } + }; render() { const { components, metric, metrics } = this.props; @@ -106,9 +85,7 @@ export default class ComponentsList extends React.PureComponent { } const otherMetrics = (complementary[metric.key] || []).map(key => metrics[key]); - const notBestComponents = components.filter( - component => !this.hasBestValue(component, otherMetrics) - ); + const notBestComponents = components.filter(component => !this.hasBestValue(component)); const hiddenCount = components.length - notBestComponents.length; const shouldHideBest = this.state.hideBest && hiddenCount !== components.length; return ( @@ -131,14 +108,21 @@ export default class ComponentsList extends React.PureComponent { )} - {(shouldHideBest ? notBestComponents : components).map(component => - this.renderComponent(component, otherMetrics) - )} + {(shouldHideBest ? notBestComponents : components).map(component => ( + + ))} - {shouldHideBest && - hiddenCount > 0 && - this.renderHiddenLink(hiddenCount, otherMetrics.length + 3)} + {shouldHideBest && hiddenCount > 0 && this.renderHiddenLink(hiddenCount)} ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.tsx similarity index 96% rename from server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.js rename to server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.tsx index 8f423a92008..9f5d7a8f4df 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.tsx @@ -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 { translate } from '../../../helpers/l10n'; export default function EmptyResult() { diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx similarity index 73% rename from server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js rename to server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx index 93f464ef47b..a4b0144ba08 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx @@ -17,36 +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 key from 'keymaster'; +import * as React from 'react'; +import * as key from 'keymaster'; import { throttle } from 'lodash'; import ComponentsList from './ComponentsList'; import ListFooter from '../../../components/controls/ListFooter'; import { scrollToElement } from '../../../helpers/scrolling'; -/*:: import type { Component, ComponentEnhanced, Paging } from '../types'; */ -/*:: import type { Metric } from '../../../store/metrics/actions'; */ +import { + ComponentMeasure, + ComponentMeasureEnhanced, + Metric, + Paging, + BranchLike +} from '../../../app/types'; -/*:: type Props = {| - bestValue?: string, - branchLike?: { id?: string; name: string }, - components: Array, - fetchMore: () => void, - handleSelect: string => void, - handleOpen: string => void, - metric: Metric, - metrics: { [string]: Metric }, - paging: ?Paging, - rootComponent: Component, - selectedKey: ?string, - selectedIdx: ?number -|}; */ +interface Props { + bestValue?: string; + branchLike?: BranchLike; + components: ComponentMeasureEnhanced[]; + fetchMore: () => void; + handleSelect: (component: string) => void; + handleOpen: (component: string) => void; + metric: Metric; + metrics: { [metric: string]: Metric }; + paging?: Paging; + rootComponent: ComponentMeasure; + selectedKey?: string; + selectedIdx?: number; +} -export default class ListView extends React.PureComponent { - /*:: listContainer: HTMLElement; */ - /*:: props: Props; */ +export default class ListView extends React.PureComponent { + listContainer?: HTMLElement | null; - constructor(props /*: Props */) { + constructor(props: Props) { super(props); this.selectNext = throttle(this.selectNext, 100); this.selectPrevious = throttle(this.selectPrevious, 100); @@ -54,13 +57,13 @@ export default class ListView extends React.PureComponent { componentDidMount() { this.attachShortcuts(); - if (this.props.selectedKey != null) { + if (this.props.selectedKey !== undefined) { this.scrollToElement(); } } - componentDidUpdate(prevProps /*: Props */) { - if (this.props.selectedKey != null && prevProps.selectedKey !== this.props.selectedKey) { + componentDidUpdate(prevProps: Props) { + if (this.props.selectedKey !== undefined && prevProps.selectedKey !== this.props.selectedKey) { this.scrollToElement(); } } @@ -85,18 +88,18 @@ export default class ListView extends React.PureComponent { } detachShortcuts() { - ['up', 'down', 'right'].map(action => key.unbind(action, 'measures-files')); + ['up', 'down', 'right'].forEach(action => key.unbind(action, 'measures-files')); } openSelected = () => { - if (this.props.selectedKey != null) { + if (this.props.selectedKey !== undefined) { this.props.handleOpen(this.props.selectedKey); } }; selectPrevious = () => { const { selectedIdx } = this.props; - if (selectedIdx != null && selectedIdx > 0) { + if (selectedIdx !== undefined && selectedIdx > 0) { this.props.handleSelect(this.props.components[selectedIdx - 1].key); } else { this.props.handleSelect(this.props.components[this.props.components.length - 1].key); @@ -105,7 +108,7 @@ export default class ListView extends React.PureComponent { selectNext = () => { const { selectedIdx } = this.props; - if (selectedIdx != null && selectedIdx < this.props.components.length - 1) { + if (selectedIdx !== undefined && selectedIdx < this.props.components.length - 1) { this.props.handleSelect(this.props.components[selectedIdx + 1].key); } else { this.props.handleSelect(this.props.components[0].key); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx similarity index 72% rename from server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js rename to server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx index ce1735a02ca..828cab940d7 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx @@ -17,107 +17,105 @@ * 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'; -// $FlowFixMe -import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'; + +import * as React from 'react'; +import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; import { scaleLinear, scaleOrdinal } from 'd3-scale'; import EmptyResult from './EmptyResult'; import * as theme from '../../../app/theme'; import ColorBoxLegend from '../../../components/charts/ColorBoxLegend'; import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; -import TreeMap from '../../../components/charts/TreeMap'; +import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap'; import { translate, translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { getBranchLikeUrl } from '../../../helpers/urls'; -/*:: import type { Metric } from '../../../store/metrics/actions'; */ -/*:: import type { ComponentEnhanced } from '../types'; */ -/*:: import type { TreeMapItem } from '../../../components/charts/TreeMap'; */ +import { BranchLike, ComponentMeasureEnhanced, Metric } from '../../../app/types'; -/*:: type Props = {| - branchLike?: { id?: string; name: string }, - components: Array, - handleSelect: string => void, - metric: Metric -|}; */ +interface Props { + branchLike?: BranchLike; + components: ComponentMeasureEnhanced[]; + handleSelect: (component: string) => void; + metric: Metric; +} -/*:: type State = { - treemapItems: Array -}; */ +interface State { + treemapItems: TreeMapItem[]; +} const HEIGHT = 500; const COLORS = [theme.green, theme.lightGreen, theme.yellow, theme.orange, theme.red]; const LEVEL_COLORS = [theme.red, theme.orange, theme.green, theme.gray71]; -export default class TreeMapView extends React.PureComponent { - /*:: props: Props; */ - /*:: state: State; */ +export default class TreeMapView extends React.PureComponent { + state: State; - constructor(props /*: Props */) { + constructor(props: Props) { super(props); this.state = { treemapItems: this.getTreemapComponents(props) }; } - componentWillReceiveProps(nextProps /*: Props */) { + componentWillReceiveProps(nextProps: Props) { if (nextProps.components !== this.props.components || nextProps.metric !== this.props.metric) { this.setState({ treemapItems: this.getTreemapComponents(nextProps) }); } } - getTreemapComponents = ({ branchLike, components, metric } /*: Props */) => { + getTreemapComponents = ({ branchLike, components, metric }: Props) => { const colorScale = this.getColorScale(metric); return components .map(component => { const colorMeasure = component.measures.find(measure => measure.metric.key === metric.key); const sizeMeasure = component.measures.find(measure => measure.metric.key !== metric.key); - if (sizeMeasure == null) { - return null; + if (!sizeMeasure) { + return undefined; } const colorValue = colorMeasure && (isDiffMetric(metric.key) ? colorMeasure.leak : colorMeasure.value); - const sizeValue = isDiffMetric(sizeMeasure.metric.key) - ? sizeMeasure.leak - : sizeMeasure.value; - if (sizeValue == null) { - return null; + const sizeValue = Number( + isDiffMetric(sizeMeasure.metric.key) ? sizeMeasure.leak : sizeMeasure.value + ); + if (isNaN(sizeValue)) { + return undefined; } + return { + color: + colorValue !== undefined ? (colorScale as Function)(colorValue) : theme.secondFontColor, + icon: , key: component.refKey || component.key, + label: component.name, + link: getBranchLikeUrl(component.refKey || component.key, branchLike), size: sizeValue, - color: colorValue != null ? colorScale(colorValue) : theme.secondFontColor, - icon: , - tooltip: this.getTooltip( - component.name, - metric, - sizeMeasure.metric, + tooltip: this.getTooltip({ + colorMetric: metric, colorValue, + componentName: component.name, + sizeMetric: sizeMeasure.metric, sizeValue - ), - label: component.name, - link: getBranchLikeUrl(component.refKey || component.key, branchLike) + }) }; }) - .filter(Boolean); + .filter(Boolean) as TreeMapItem[]; }; getLevelColorScale = () => - scaleOrdinal() + scaleOrdinal() .domain(['ERROR', 'WARN', 'OK', 'NONE']) .range(LEVEL_COLORS); - getPercentColorScale = (metric /*: Metric */) => { - const color = scaleLinear().domain([0, 25, 50, 75, 100]); - color.range(metric.direction === 1 ? COLORS.reverse() : COLORS); + getPercentColorScale = (metric: Metric) => { + const color = scaleLinear().domain([0, 25, 50, 75, 100]); + color.range(metric.direction === 1 ? [...COLORS].reverse() : COLORS); return color; }; getRatingColorScale = () => - scaleLinear() + scaleLinear() .domain([1, 2, 3, 4, 5]) .range(COLORS); - getColorScale = (metric /*: Metric */) => { + getColorScale = (metric: Metric) => { if (metric.type === 'LEVEL') { return this.getLevelColorScale(); } @@ -127,15 +125,21 @@ export default class TreeMapView extends React.PureComponent { return this.getPercentColorScale(metric); }; - getTooltip = ( - componentName /*: string */, - colorMetric /*: Metric */, - sizeMetric /*: Metric */, - colorValue /*: ?number */, - sizeValue /*: number */ - ) => { + getTooltip = ({ + colorMetric, + colorValue, + componentName, + sizeMetric, + sizeValue + }: { + colorMetric: Metric; + colorValue?: string; + componentName: string; + sizeMetric: Metric; + sizeValue: number; + }) => { const formatted = - colorMetric != null && colorValue != null ? formatMeasure(colorValue, colorMetric.type) : '—'; + colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—'; return (
{componentName} diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.js b/server/sonar-web/src/main/js/apps/component-measures/utils.ts similarity index 66% rename from server/sonar-web/src/main/js/apps/component-measures/utils.js rename to server/sonar-web/src/main/js/apps/component-measures/utils.ts index d822e66e674..553edee41f4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.js +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts @@ -17,17 +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. */ -// @flow import { groupBy, memoize, sortBy, toPairs } from 'lodash'; import { domains } from './config/domains'; import { bubbles } from './config/bubbles'; import { getLocalizedMetricName } from '../../helpers/l10n'; -import { cleanQuery, parseAsString, serializeString } from '../../helpers/query'; +import { ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../app/types'; import { enhanceMeasure } from '../../components/measure/utils'; -/*:: import type { Component, ComponentEnhanced, Query } from './types'; */ -/*:: import type { RawQuery } from '../../helpers/query'; */ -/*:: import type { Metric } from '../../store/metrics/actions'; */ -/*:: import type { MeasureEnhanced } from '../../components/measure/types'; */ +import { cleanQuery, parseAsString, RawQuery, serializeString } from '../../helpers/query'; +import { MeasureEnhanced } from '../../helpers/measures'; export const PROJECT_OVERVEW = 'project_overview'; export const DEFAULT_VIEW = 'list'; @@ -55,33 +52,32 @@ const BANNED_MEASURES = [ 'new_info_violations' ]; -export function filterMeasures( - measures /*: Array */ -) /*: Array */ { +export function filterMeasures(measures: MeasureEnhanced[]): MeasureEnhanced[] { return measures.filter(measure => !BANNED_MEASURES.includes(measure.metric.key)); } export function sortMeasures( - domainName /*: string */, - measures /*: Array */ -) /*: Array */ { + domainName: string, + measures: Array +): Array { const config = domains[domainName] || {}; const configOrder = config.order || []; return sortBy(measures, [ - item => { + (item: MeasureEnhanced | string) => { if (typeof item === 'string') { return configOrder.indexOf(item); } const idx = configOrder.indexOf(item.metric.key); return idx >= 0 ? idx : configOrder.length; }, - item => (typeof item === 'string' ? item : getLocalizedMetricName(item.metric)) + (item: MeasureEnhanced | string) => + typeof item === 'string' ? item : getLocalizedMetricName(item.metric) ]); } export function addMeasureCategories( - domainName /*: string */, - measures /*: Array */ + domainName: string, + measures: MeasureEnhanced[] ) /*: Array */ { const categories = domains[domainName] && domains[domainName].categories; if (categories && categories.length > 0) { @@ -91,34 +87,37 @@ export function addMeasureCategories( } export function enhanceComponent( - component /*: Component */, - metric /*: ?Metric */, - metrics /*: { [string]: Metric } */ -) /*: ComponentEnhanced */ { + component: ComponentMeasure, + metric: Metric | undefined, + metrics: { [key: string]: Metric } +): ComponentMeasureEnhanced { + if (!component.measures) { + return { ...component, measures: [] }; + } + const enhancedMeasures = component.measures.map(measure => enhanceMeasure(measure, metrics)); - // $FlowFixMe metric can't be null since there is a guard for it const measure = metric && enhancedMeasures.find(measure => measure.metric.key === metric.key); - const value = measure ? measure.value : null; - const leak = measure ? measure.leak : null; + const value = measure && measure.value; + const leak = measure && measure.leak; return { ...component, value, leak, measures: enhancedMeasures }; } -export function isFileType(component /*: Component */) /*: boolean */ { +export function isFileType(component: ComponentMeasure): boolean { return ['FIL', 'UTS'].includes(component.qualifier); } -export function isViewType(component /*: Component */) /*: boolean */ { +export function isViewType(component: ComponentMeasure): boolean { return ['VW', 'SVW', 'APP'].includes(component.qualifier); } -export const groupByDomains = memoize((measures /*: Array */) => { +export const groupByDomains = memoize((measures: MeasureEnhanced[]) => { const domains = toPairs(groupBy(measures, measure => measure.metric.domain)).map(r => ({ name: r[0], measures: r[1] })); return sortBy(domains, [ - domain => { + (domain: { name: string; measure: MeasureEnhanced[] }) => { const idx = KNOWN_DOMAINS.indexOf(domain.name); return idx >= 0 ? idx : KNOWN_DOMAINS.length; }, @@ -126,34 +125,34 @@ export const groupByDomains = memoize((measures /*: Array */) = ]); }); -export function getDefaultView(metric /*: string */) /*: string */ { +export function getDefaultView(metric: string): string { if (!hasList(metric)) { return 'tree'; } return DEFAULT_VIEW; } -export function hasList(metric /*: string */) /*: boolean */ { +export function hasList(metric: string): boolean { return !['releasability_rating', 'releasability_effort'].includes(metric); } -export function hasTree(metric /*: string */) /*: boolean */ { +export function hasTree(metric: string): boolean { return metric !== 'alert_status'; } -export function hasTreemap(metric /*: string */, type /*: string */) /*: boolean */ { +export function hasTreemap(metric: string, type: string): boolean { return ['PERCENT', 'RATING', 'LEVEL'].includes(type) && hasTree(metric); } -export function hasBubbleChart(domainName /*: string */) /*: boolean */ { +export function hasBubbleChart(domainName: string): boolean { return bubbles[domainName] != null; } -export function hasFacetStat(metric /*: string */) /*: boolean */ { +export function hasFacetStat(metric: string): boolean { return metric !== 'alert_status'; } -export function getBubbleMetrics(domain /*: string */, metrics /*: { [string]: Metric } */) { +export function getBubbleMetrics(domain: string, metrics: { [key: string]: Metric }) { const conf = bubbles[domain]; return { x: metrics[conf.x], @@ -163,15 +162,15 @@ export function getBubbleMetrics(domain /*: string */, metrics /*: { [string]: M }; } -export function getBubbleYDomain(domain /*: string */) { +export function getBubbleYDomain(domain: string) { return bubbles[domain].yDomain; } -export function isProjectOverview(metric /*: string */) { +export function isProjectOverview(metric: string) { return metric === PROJECT_OVERVEW; } -const parseView = memoize((rawView /*:: ? */ /*: string */, metric /*: string */) => { +const parseView = (metric: string, rawView?: string) => { const view = parseAsString(rawView) || DEFAULT_VIEW; if (!hasTree(metric)) { return 'list'; @@ -179,18 +178,24 @@ const parseView = memoize((rawView /*:: ? */ /*: string */, metric /*: string */ return 'tree'; } return view; -}); +}; + +export interface Query { + metric?: string; + selected?: string; + view: string; +} -export const parseQuery = memoize((urlQuery /*: RawQuery */) => { +export const parseQuery = memoize((urlQuery: RawQuery) => { const metric = parseAsString(urlQuery['metric']) || DEFAULT_METRIC; return { metric, selected: parseAsString(urlQuery['selected']), - view: parseView(urlQuery['view'], metric) + view: parseView(metric, urlQuery['view']) }; }); -export const serializeQuery = memoize((query /*: Query */) => { +export const serializeQuery = memoize((query: Query) => { return cleanQuery({ metric: query.metric === DEFAULT_METRIC ? null : serializeString(query.metric), selected: serializeString(query.selected), diff --git a/server/sonar-web/src/main/js/components/charts/ColorGradientLegend.tsx b/server/sonar-web/src/main/js/components/charts/ColorGradientLegend.tsx index ae2eb08bc30..29fe3a07492 100644 --- a/server/sonar-web/src/main/js/components/charts/ColorGradientLegend.tsx +++ b/server/sonar-web/src/main/js/components/charts/ColorGradientLegend.tsx @@ -26,7 +26,7 @@ interface Props { colorNA?: string; colorScale: | ScaleOrdinal // used for LEVEL type - | ScaleLinear; // used for RATING or PERCENT type + | ScaleLinear; // used for RATING or PERCENT type direction?: number; height: number; padding?: [number, number, number, number]; diff --git a/server/sonar-web/src/main/js/components/charts/TreeMap.tsx b/server/sonar-web/src/main/js/components/charts/TreeMap.tsx index 489617142f3..5116b3b726e 100644 --- a/server/sonar-web/src/main/js/components/charts/TreeMap.tsx +++ b/server/sonar-web/src/main/js/components/charts/TreeMap.tsx @@ -23,12 +23,12 @@ import TreeMapRect from './TreeMapRect'; import { translate } from '../../helpers/l10n'; import './TreeMap.css'; -interface TreeMapItem { +export interface TreeMapItem { color: string; icon?: React.ReactNode; key: string; label: string; - link?: string; + link?: string | Location; size: number; tooltip?: React.ReactNode; } diff --git a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx index 75e8cdc8db5..3c3341028b5 100644 --- a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx +++ b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx @@ -35,7 +35,7 @@ interface Props { icon?: React.ReactNode; itemKey: string; label: string; - link?: string; + link?: string | Location; onClick?: (item: string) => void; placement?: Placement; prefix: string; diff --git a/server/sonar-web/src/main/js/helpers/measures.ts b/server/sonar-web/src/main/js/helpers/measures.ts index 596ea2a3685..171e04121f2 100644 --- a/server/sonar-web/src/main/js/helpers/measures.ts +++ b/server/sonar-web/src/main/js/helpers/measures.ts @@ -23,14 +23,15 @@ import { Metric } from '../app/types'; const HOURS_IN_DAY = 8; export interface MeasurePeriod { + bestValue?: boolean; index: number; value: string; - bestValue?: boolean; } export interface MeasureIntern { - value?: string; + bestValue?: boolean; periods?: MeasurePeriod[]; + value?: string; } export interface Measure extends MeasureIntern { -- 2.39.5