From: Grégoire Aubert Date: Wed, 17 Jan 2018 10:29:05 +0000 (+0100) Subject: Migrate parts of overview app to TS X-Git-Tag: 7.5~1777 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=df96a05cc325b2946c25ee8277f64638ed72288c;p=sonarqube.git Migrate parts of overview app to TS --- diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 5ed1cf5a258..db1e32cc643 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -79,7 +79,7 @@ export function createProject(data: { } export function searchProjectTags(data?: { ps?: number; q?: string }): Promise { - return getJSON('/api/project_tags/search', data); + return getJSON('/api/project_tags/search', data).catch(throwGlobalError); } export function setProjectTags(data: { project: string; tags: string }): Promise { diff --git a/server/sonar-web/src/main/js/api/projectActivity.ts b/server/sonar-web/src/main/js/api/projectActivity.ts index d78ff17681e..b931ea5a6f6 100644 --- a/server/sonar-web/src/main/js/api/projectActivity.ts +++ b/server/sonar-web/src/main/js/api/projectActivity.ts @@ -19,14 +19,19 @@ */ import { getJSON, postJSON, post, RequestData } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; +import { Paging } from '../app/types'; -interface GetProjectActivityResponse { - analyses: any[]; - paging: { - total: number; - pageIndex: number; - pageSize: number; - }; +export interface Event { + key: string; + name: string; + category: string; + description?: string; +} + +export interface Analysis { + key: string; + date: string; + events: Event[]; } export function getProjectActivity(data: { @@ -35,7 +40,7 @@ export function getProjectActivity(data: { category?: string; p?: number; ps?: number; -}): Promise { +}): Promise<{ analyses: Analysis[]; paging: Paging }> { return getJSON('/api/project_analyses/search', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/projectLinks.ts b/server/sonar-web/src/main/js/api/projectLinks.ts index 16a492bca9f..e91b200c4fe 100644 --- a/server/sonar-web/src/main/js/api/projectLinks.ts +++ b/server/sonar-web/src/main/js/api/projectLinks.ts @@ -18,11 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { getJSON, post, postJSON } from '../helpers/request'; +import throwGlobalError from '../app/utils/throwGlobalError'; -export function getProjectLinks(projectKey: string): Promise { +export interface ProjectLink { + id: string; + name: string; + type: string; + url: string; +} + +export function getProjectLinks(projectKey: string): Promise { const url = '/api/project_links/search'; const data = { projectKey }; - return getJSON(url, data).then(r => r.links); + return getJSON(url, data).then(r => r.links, throwGlobalError); } export function deleteLink(linkId: string): Promise { diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts index 5a50564b36f..b40e6130b28 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -19,6 +19,7 @@ */ import { getJSON, post, postJSON } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; +import { Metric } from '../app/types'; export interface ConditionBase { error: string; @@ -148,9 +149,33 @@ export function dissociateGateWithProject(data: { return post('/api/qualitygates/deselect', data).catch(throwGlobalError); } +export interface ConditionAnalysis { + comparator: string; + errorThreshold?: string; + metric: string; + periodIndex?: number; + onLeak?: boolean; + status: string; + value: string; + warningThreshold?: string; +} + +export interface ApplicationProject { + key: string; + name: string; + status: string; + conditions: ConditionAnalysis[]; +} + +export interface ApplicationQualityGate { + metrics: Metric[]; + projects: ApplicationProject[]; + status: string; +} + export function getApplicationQualityGate(data: { application: string; organization?: string; -}): Promise { +}): Promise { return getJSON('/api/qualitygates/application_status', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/time-machine.ts b/server/sonar-web/src/main/js/api/time-machine.ts index e6c01eba852..2f25912b191 100644 --- a/server/sonar-web/src/main/js/api/time-machine.ts +++ b/server/sonar-web/src/main/js/api/time-machine.ts @@ -19,6 +19,7 @@ */ import { getJSON } from '../helpers/request'; import { Paging } from '../app/types'; +import throwGlobalError from '../app/utils/throwGlobalError'; export interface HistoryItem { date: Date; @@ -47,7 +48,7 @@ export function getTimeMachineData( metrics: metrics.join(), ps: 1000, ...other - }); + }).catch(throwGlobalError); } export function getAllTimeMachineData( diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index e109a142861..de3102a3957 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -43,7 +43,7 @@ interface Props { interface State { branches: Branch[]; loading: boolean; - component: Component | null; + component?: Component; currentTask?: Task; isInProgress?: boolean; isPending?: boolean; @@ -54,7 +54,7 @@ export class ComponentContainer extends React.PureComponent { constructor(props: Props) { super(props); - this.state = { branches: [], loading: true, component: null }; + this.state = { branches: [], loading: true }; } componentDidMount() { diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 7a2a64d4e26..dedfabe68c2 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -68,19 +68,25 @@ export interface Breadcrumb { qualifier: string; } -export interface Component { +export interface LightComponent { + key: string; + organization: string; + qualifier: string; +} + +export interface Component extends LightComponent { analysisDate?: string; breadcrumbs: Breadcrumb[]; configuration?: ComponentConfiguration; description?: string; extensions?: Extension[]; isFavorite?: boolean; - key: string; name: string; - organization: string; path?: string; - qualifier: string; refKey?: string; + qualityProfiles?: { key: string; language: string; name: string }[]; + qualityGate?: { isDefault?: boolean; key: string; name: string }; + tags?: string[]; version?: string; visibility?: string; } @@ -105,7 +111,7 @@ export interface Metric { domain?: string; hidden?: boolean; key: string; - name?: string; + name: string; qualitative?: boolean; type: string; } diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx index d0bdd18e303..bc900211a0e 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx @@ -42,7 +42,5 @@ export default function ComponentMeasure({ component, metricKey, metricType }: P return ; } - return ( - - ); + return ; } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js index f59b58ee56f..c7964eb56a1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js @@ -44,7 +44,7 @@ import { isDiffMetric } from '../../../helpers/measures'; export default function MeasureHeader(props /*: Props*/) { const { branch, component, leakPeriod, measure, secondaryMeasure } = props; - const metric = measure.metric; + const { metric } = measure; const isDiff = isDiffMetric(metric.key); return (
@@ -55,9 +55,14 @@ export default function MeasureHeader(props /*: Props*/) { {isDiff ? ( - + ) : ( - + )} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap index e58ba900cd7..d6c9bceb55a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap @@ -68,30 +68,9 @@ exports[`should render correctly 1`] = ` > @@ -165,29 +144,9 @@ exports[`should render correctly for leak 1`] = ` diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js index 30b74e4aab4..e87b224967f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js @@ -52,11 +52,8 @@ export default function ComponentsListRow(props /*: Props */) { {otherMeasures.map(measure => ( ))} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js index d5109091646..0934587ccf2 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.js @@ -20,6 +20,7 @@ // @flow import React from 'react'; import Measure from '../../../components/measure/Measure'; +import { isDiffMetric } from '../../../helpers/measures'; /*:: import type { Component } from '../types'; */ /*:: import type { Metric } from '../../../store/metrics/actions'; */ @@ -32,7 +33,11 @@ export default function MeasureCell({ component, metric } /*: Props */) { return ( - + ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js index d4060ac29c7..93d7ff58493 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.js @@ -29,14 +29,22 @@ export default function FacetMeasureValue({ measure } /*: { measure: MeasureEnha
- +
); } return (
- +
); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap index 9bd6a3e7e00..228f8a3e6d9 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetMeasureValue-test.js.snap @@ -6,23 +6,9 @@ exports[`should display leak measure value 1`] = ` id="measure-new_bugs-leak" >
`; @@ -33,24 +19,9 @@ exports[`should display measure value 1`] = ` id="measure-bugs-value" > `; diff --git a/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx b/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx index d8d23b2182c..7e4e0daf249 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx +++ b/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx @@ -27,7 +27,7 @@ import { translate } from '../../../helpers/l10n'; import './styles.css'; interface Props { - branch: string; + branch?: string; project: string; } diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx new file mode 100644 index 00000000000..03d898d4e9e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx @@ -0,0 +1,86 @@ +/* + * 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 * as React from 'react'; +import * as PropTypes from 'prop-types'; +import OverviewApp from './OverviewApp'; +import EmptyOverview from './EmptyOverview'; +import { getBranchName, isShortLivingBranch } from '../../../helpers/branches'; +import { getProjectBranchUrl, getCodeUrl } from '../../../helpers/urls'; +import { Branch, Component } from '../../../app/types'; + +interface Props { + branch?: Branch; + component: Component; + isInProgress?: boolean; + isPending?: boolean; + onComponentChange: (changes: Partial) => void; +} + +export default class App extends React.PureComponent { + static contextTypes = { + router: PropTypes.object + }; + + componentDidMount() { + const { branch, component } = this.props; + + if (this.isPortfolio()) { + this.context.router.replace({ + pathname: '/portfolio', + query: { id: component.key } + }); + } else if (this.isFile()) { + this.context.router.replace( + getCodeUrl(component.breadcrumbs[0].key, getBranchName(branch), component.key) + ); + } else if (isShortLivingBranch(branch)) { + this.context.router.replace(getProjectBranchUrl(component.key, branch)); + } + } + + isPortfolio = () => ['VW', 'SVW'].includes(this.props.component.qualifier); + + isFile = () => ['FIL', 'UTS'].includes(this.props.component.qualifier); + + render() { + const { branch, component } = this.props; + + if (this.isPortfolio() || this.isFile() || isShortLivingBranch(branch)) { + return null; + } + + if (!component.analysisDate) { + return ( + + ); + } + + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js deleted file mode 100644 index 0271e1ecdc1..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ /dev/null @@ -1,202 +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. - */ -// @flow -import React from 'react'; -import { uniq } from 'lodash'; -import QualityGate from '../qualityGate/QualityGate'; -import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate'; -import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities'; -import CodeSmells from '../main/CodeSmells'; -import Coverage from '../main/Coverage'; -import Duplications from '../main/Duplications'; -import Meta from '../meta/Meta'; -import throwGlobalError from '../../../app/utils/throwGlobalError'; -import { getMeasuresAndMeta } from '../../../api/measures'; -import { getAllTimeMachineData } from '../../../api/time-machine'; -import { parseDate } from '../../../helpers/dates'; -import { enhanceMeasuresWithMetrics } from '../../../helpers/measures'; -import { getLeakPeriod } from '../../../helpers/periods'; -import { getCustomGraph, getGraph } from '../../../helpers/storage'; -import { METRICS, HISTORY_METRICS_LIST } from '../utils'; -import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils'; -import { getBranchName } from '../../../helpers/branches'; -/*:: import type { Component, History, MeasuresList, Period } from '../types'; */ -import '../styles.css'; - -/*:: -type Props = { - branch?: { name: string }, - component: Component, - onComponentChange: {} => void -}; -*/ - -/*:: -type State = { - history?: History, - historyStartDate?: Date, - loading: boolean, - measures: MeasuresList, - periods?: Array -}; -*/ - -export default class OverviewApp extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: props: Props; */ - state /*: State */ = { - loading: true, - measures: [] - }; - - componentDidMount() { - this.mounted = true; - this.loadMeasures().then(this.loadHistory); - } - - componentDidUpdate(prevProps /*: Props */) { - if ( - this.props.component.key !== prevProps.component.key || - this.props.branch !== prevProps.branch - ) { - this.loadMeasures().then(this.loadHistory); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - loadMeasures() { - const { branch, component } = this.props; - this.setState({ loading: true }); - - return getMeasuresAndMeta(component.key, METRICS, { - additionalFields: 'metrics,periods', - branch: branch && getBranchName(branch) - }).then( - r => { - if (this.mounted) { - this.setState({ - loading: false, - measures: enhanceMeasuresWithMetrics(r.component.measures, r.metrics), - periods: r.periods - }); - } - }, - error => { - throwGlobalError(error); - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - } - - loadHistory = () => { - const { branch, component } = this.props; - - let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph()); - if (!graphMetrics || graphMetrics.length <= 0) { - graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []); - } - - const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics)); - return getAllTimeMachineData(component.key, metrics, { - branch: branch && getBranchName(branch) - }).then(r => { - if (this.mounted) { - const history /*: History */ = {}; - r.measures.forEach(measure => { - const measureHistory = measure.history.map(analysis => ({ - date: parseDate(analysis.date), - value: analysis.value - })); - history[measure.metric] = measureHistory; - }); - const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date; - this.setState({ history, historyStartDate }); - } - }, throwGlobalError); - }; - - getApplicationLeakPeriod = () => - this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null; - - renderLoading() { - return ( -
- -
- ); - } - - render() { - const { branch, component } = this.props; - const { loading, measures, periods, history, historyStartDate } = this.state; - - if (loading) { - return this.renderLoading(); - } - - const leakPeriod = - component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods); - const branchName = branch && getBranchName(branch); - const domainProps = { - branch: branchName, - component, - measures, - leakPeriod, - history, - historyStartDate - }; - - return ( -
-
-
- {component.qualifier === 'APP' ? ( - - ) : ( - - )} - -
- - - - -
-
- -
- -
-
-
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx new file mode 100644 index 00000000000..07d22017edf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx @@ -0,0 +1,193 @@ +/* + * 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 * as React from 'react'; +import { uniq } from 'lodash'; +import QualityGate from '../qualityGate/QualityGate'; +import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate'; +import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities'; +import CodeSmells from '../main/CodeSmells'; +import Coverage from '../main/Coverage'; +import Duplications from '../main/Duplications'; +import Meta from '../meta/Meta'; +import throwGlobalError from '../../../app/utils/throwGlobalError'; +import { getMeasuresAndMeta } from '../../../api/measures'; +import { getAllTimeMachineData, History } from '../../../api/time-machine'; +import { parseDate } from '../../../helpers/dates'; +import { enhanceMeasuresWithMetrics, MeasureEnhanced } from '../../../helpers/measures'; +import { getLeakPeriod, Period } from '../../../helpers/periods'; +import { getCustomGraph, getGraph } from '../../../helpers/storage'; +import { METRICS, HISTORY_METRICS_LIST } from '../utils'; +import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils'; +import { getBranchName } from '../../../helpers/branches'; +import { Branch, Component } from '../../../app/types'; +import '../styles.css'; + +interface Props { + branch?: Branch; + component: Component; + onComponentChange: (changes: {}) => void; +} + +interface State { + history?: History; + historyStartDate?: Date; + loading: boolean; + measures: MeasureEnhanced[]; + periods?: Period[]; +} + +export default class OverviewApp extends React.PureComponent { + mounted: boolean; + state: State = { loading: true, measures: [] }; + + componentDidMount() { + this.mounted = true; + this.loadMeasures().then(this.loadHistory, () => {}); + } + + componentDidUpdate(prevProps: Props) { + if ( + this.props.component.key !== prevProps.component.key || + this.props.branch !== prevProps.branch + ) { + this.loadMeasures().then(this.loadHistory, () => {}); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + loadMeasures() { + const { branch, component } = this.props; + this.setState({ loading: true }); + + return getMeasuresAndMeta(component.key, METRICS, { + additionalFields: 'metrics,periods', + branch: getBranchName(branch) + }).then( + r => { + if (this.mounted && r.metrics) { + this.setState({ + loading: false, + measures: enhanceMeasuresWithMetrics(r.component.measures, r.metrics), + periods: r.periods + }); + } + }, + error => { + throwGlobalError(error); + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + } + + loadHistory = () => { + const { branch, component } = this.props; + + let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph()); + if (!graphMetrics || graphMetrics.length <= 0) { + graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []); + } + + const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics)); + return getAllTimeMachineData(component.key, metrics, { branch: getBranchName(branch) }).then( + r => { + if (this.mounted) { + const history: History = {}; + r.measures.forEach(measure => { + const measureHistory = measure.history.map(analysis => ({ + date: parseDate(analysis.date), + value: analysis.value + })); + history[measure.metric] = measureHistory; + }); + const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date; + this.setState({ history, historyStartDate }); + } + } + ); + }; + + getApplicationLeakPeriod = () => + this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null; + + renderLoading() { + return ( +
+ +
+ ); + } + + render() { + const { branch, component } = this.props; + const { loading, measures, periods, history, historyStartDate } = this.state; + + if (loading) { + return this.renderLoading(); + } + + const leakPeriod = + component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods); + const branchName = getBranchName(branch); + const domainProps = { + branch: branchName, + component, + measures, + leakPeriod, + history, + historyStartDate + }; + + return ( +
+
+
+ {component.qualifier === 'APP' ? ( + + ) : ( + + )} + +
+ + + + +
+
+ +
+ +
+
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/Timeline.js b/server/sonar-web/src/main/js/apps/overview/components/Timeline.js deleted file mode 100644 index 15dde5221d3..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/components/Timeline.js +++ /dev/null @@ -1,68 +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 React from 'react'; -import PropTypes from 'prop-types'; -import { max } from 'd3-array'; -import { LineChart } from '../../../components/charts/line-chart'; - -const HEIGHT = 80; - -export default class Timeline extends React.PureComponent { - static propTypes = { - history: PropTypes.arrayOf(PropTypes.object).isRequired, - before: PropTypes.object, - after: PropTypes.object - }; - - filterSnapshots() { - const { history, before, after } = this.props; - - return history.filter(s => { - const matchBefore = !before || s.date <= before; - const matchAfter = !after || s.date >= after; - return matchBefore && matchAfter; - }); - } - - render() { - const snapshots = this.filterSnapshots(); - - if (snapshots.length < 2) { - return null; - } - - const data = snapshots.map((snapshot, index) => { - return { x: index, y: snapshot.value }; - }); - const domain = [0, max(this.props.history, d => parseFloat(d.value))]; - return ( - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/components/Timeline.tsx b/server/sonar-web/src/main/js/apps/overview/components/Timeline.tsx new file mode 100644 index 00000000000..1b2bd3d04ec --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/Timeline.tsx @@ -0,0 +1,68 @@ +/* + * 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 * as React from 'react'; +import { max } from 'd3-array'; +import { LineChart } from '../../../components/charts/line-chart'; +import { HistoryItem } from '../../../api/time-machine'; + +const HEIGHT = 80; + +interface Props { + history: HistoryItem[]; + before?: Date; + after?: Date; +} + +export default class Timeline extends React.PureComponent { + filterSnapshots() { + const { history, before, after } = this.props; + + return history.filter(s => { + const matchBefore = !before || s.date <= before; + const matchAfter = !after || s.date >= after; + return matchBefore && matchAfter; + }); + } + + render() { + const snapshots = this.filterSnapshots(); + + if (snapshots.length < 2) { + return null; + } + + const data = snapshots.map((snapshot, index) => { + return { x: index, y: snapshot.value }; + }); + const domain = [0, max(this.props.history, d => parseFloat(d.value))]; + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx new file mode 100644 index 00000000000..4edd63ffba8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx @@ -0,0 +1,68 @@ +/* + * 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 * as React from 'react'; +import { mount, shallow } from 'enzyme'; +import App from '../App'; +import OverviewApp from '../OverviewApp'; +import EmptyOverview from '../EmptyOverview'; +import { BranchType, LongLivingBranch } from '../../../../app/types'; + +const component = { + key: 'foo', + analysisDate: '2016-01-01', + breadcrumbs: [], + name: 'Foo', + organization: 'org', + qualifier: 'TRK', + version: '0.0.1' +}; + +it('should render OverviewApp', () => { + expect(getWrapper().type()).toBe(OverviewApp); +}); + +it('should render EmptyOverview', () => { + const output = getWrapper({ component: { key: 'foo' } }); + expect(output.type()).toBe(EmptyOverview); +}); + +it('redirects on Code page for files', () => { + const branch: LongLivingBranch = { isMain: false, name: 'b', type: BranchType.LONG }; + const newComponent = { + ...component, + breadcrumbs: [ + { key: 'project', name: 'Project', qualifier: 'TRK' }, + { key: 'foo', name: 'Foo', qualifier: 'DIR' } + ], + qualifier: 'FIL' + }; + const replace = jest.fn(); + mount(, { + context: { router: { replace } } + }); + expect(replace).toBeCalledWith({ + pathname: '/code', + query: { branch: 'b', id: 'project', selected: 'foo' } + }); +}); + +function getWrapper(props = {}) { + return shallow(); +} diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js deleted file mode 100644 index bff7894acb7..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js +++ /dev/null @@ -1,155 +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. - */ -// @flow -import React from 'react'; -import { Link } from 'react-router'; -import Analysis from './Analysis'; -import { getAllMetrics } from '../../../api/metrics'; -import { getProjectActivity } from '../../../api/projectActivity'; -import PreviewGraph from '../../../components/preview-graph/PreviewGraph'; -import { translate } from '../../../helpers/l10n'; -/*:: import type { Analysis as AnalysisType } from '../../projectActivity/types'; */ -/*:: import type { History, Metric } from '../types'; */ - -/*:: -type Props = { - branch?: string, - component: Object, - history: ?History, - qualifier: string -}; -*/ - -/*:: -type State = { - analyses: Array, - loading: boolean, - metrics: Array -}; -*/ - -const PAGE_SIZE = 3; - -export default class AnalysesList extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: props: Props; */ - state /*: State */ = { analyses: [], loading: true, metrics: [] }; - - componentDidMount() { - this.mounted = true; - this.fetchData(); - } - - componentDidUpdate(prevProps /*: Props */) { - if (prevProps.component !== this.props.component) { - this.fetchData(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - getTopLevelComponent = () => { - const { component } = this.props; - let current = component.breadcrumbs.length - 1; - while ( - current > 0 && - !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier) - ) { - current--; - } - return component.breadcrumbs[current].key; - }; - - fetchData() { - this.setState({ loading: true }); - Promise.all([ - getProjectActivity({ - branch: this.props.branch, - project: this.getTopLevelComponent(), - ps: PAGE_SIZE - }), - getAllMetrics() - ]).then( - response => { - if (this.mounted) { - this.setState({ - analyses: response[0].analyses, - metrics: response[1], - loading: false - }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - } - - renderList(analyses /*: Array */) { - if (!analyses.length) { - return

{translate('no_results')}

; - } - - return ( -
    - {analyses.map(analysis => ( - - ))} -
- ); - } - - render() { - const { analyses, loading } = this.state; - - if (loading) { - return null; - } - - return ( -
-

{translate('project_activity.page')}

- - - - {this.renderList(analyses)} - -
- - {translate('show_more')} - -
-
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx new file mode 100644 index 00000000000..30a3d98e7b1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx @@ -0,0 +1,145 @@ +/* + * 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 * as React from 'react'; +import { Link } from 'react-router'; +import Analysis from './Analysis'; +import { getAllMetrics } from '../../../api/metrics'; +import { getProjectActivity, Analysis as IAnalysis } from '../../../api/projectActivity'; +import PreviewGraph from '../../../components/preview-graph/PreviewGraph'; +import { translate } from '../../../helpers/l10n'; +import { Metric, Component } from '../../../app/types'; +import { History } from '../../../api/time-machine'; + +interface Props { + branch?: string; + component: Component; + history?: History; + qualifier: string; +} + +interface State { + analyses: IAnalysis[]; + loading: boolean; + metrics: Metric[]; +} + +const PAGE_SIZE = 3; + +export default class AnalysesList extends React.PureComponent { + mounted: boolean; + state: State = { analyses: [], loading: true, metrics: [] }; + + componentDidMount() { + this.mounted = true; + this.fetchData(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.component !== this.props.component) { + this.fetchData(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + getTopLevelComponent = () => { + const { component } = this.props; + let current = component.breadcrumbs.length - 1; + while ( + current > 0 && + !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier) + ) { + current--; + } + return component.breadcrumbs[current].key; + }; + + fetchData = () => { + this.setState({ loading: true }); + Promise.all([ + getProjectActivity({ + branch: this.props.branch, + project: this.getTopLevelComponent(), + ps: PAGE_SIZE + }), + getAllMetrics() + ]).then( + ([{ analyses }, metrics]) => { + if (this.mounted) { + this.setState({ analyses, metrics, loading: false }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + renderList(analyses: IAnalysis[]) { + if (!analyses.length) { + return

{translate('no_results')}

; + } + + return ( +
    + {analyses.map(analysis => ( + + ))} +
+ ); + } + + render() { + const { analyses, loading } = this.state; + + if (loading) { + return null; + } + + return ( +
+

{translate('project_activity.page')}

+ + + + {this.renderList(analyses)} + +
+ + {translate('show_more')} + +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js deleted file mode 100644 index 3addf419677..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js +++ /dev/null @@ -1,65 +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. - */ -// @flow -import React from 'react'; -import { sortBy } from 'lodash'; -import Event from './Event'; -import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter'; -import { translate } from '../../../helpers/l10n'; -/*:: import type { Analysis as AnalysisType, Event as EventType } from '../../projectActivity/types'; */ - -/*:: -type Props = { - analysis: AnalysisType, - qualifier: string -}; -*/ - -export default function Analysis(props /*: Props */) { - const { analysis } = props; - const sortedEvents /*: Array */ = sortBy( - analysis.events, - // versions first - (event /*: EventType */) => (event.category === 'VERSION' ? 0 : 1), - // then the rest sorted by category - 'category' - ); - - // use `TRK` for all components but applications - const qualifier = props.qualifier === 'APP' ? 'APP' : 'TRK'; - - return ( -
  • -
    - - - -
    - - {sortedEvents.length > 0 ? ( -
    - {sortedEvents.map(event => )} -
    - ) : ( - {translate('project_activity.analyzed', qualifier)} - )} -
  • - ); -} diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.tsx b/server/sonar-web/src/main/js/apps/overview/events/Analysis.tsx new file mode 100644 index 00000000000..f80ce5646b8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/Analysis.tsx @@ -0,0 +1,61 @@ +/* + * 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 * as React from 'react'; +import { sortBy } from 'lodash'; +import Event from './Event'; +import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter'; +import { Analysis as IAnalysis, Event as IEvent } from '../../../api/projectActivity'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + analysis: IAnalysis; + qualifier: string; +} + +export default function Analysis({ analysis, ...props }: Props) { + const sortedEvents: Array = sortBy( + analysis.events, + // versions first + (event: IEvent) => (event.category === 'VERSION' ? 0 : 1), + // then the rest sorted by category + 'category' + ); + + // use `TRK` for all components but applications + const qualifier = props.qualifier === 'APP' ? 'APP' : 'TRK'; + + return ( +
  • +
    + + + +
    + + {sortedEvents.length > 0 ? ( +
    + {sortedEvents.map(event => )} +
    + ) : ( + {translate('project_activity.analyzed', qualifier)} + )} +
  • + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.js b/server/sonar-web/src/main/js/apps/overview/events/Event.js deleted file mode 100644 index ca2982c0d0b..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/events/Event.js +++ /dev/null @@ -1,49 +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. - */ -// @flow -import React from 'react'; -import Tooltip from '../../../components/controls/Tooltip'; -import { translate } from '../../../helpers/l10n'; -/*:: import type { Event as EventType } from '../../projectActivity/types'; */ - -export default function Event(props /*: { event: EventType } */) { - const { event } = props; - - if (event.category === 'VERSION') { - return ( - - {props.event.name} - - ); - } - - return ( -
    - {translate('event.category', event.category)}:{' '} - {event.description ? ( - - {event.name} - - ) : ( - {event.name} - )} -
    - ); -} diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.tsx b/server/sonar-web/src/main/js/apps/overview/events/Event.tsx new file mode 100644 index 00000000000..f90d318845a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/Event.tsx @@ -0,0 +1,50 @@ +/* + * 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 * as React from 'react'; +import Tooltip from '../../../components/controls/Tooltip'; +import { Event as IEvent } from '../../../api/projectActivity'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + event: IEvent; +} + +export default function Event({ event }: Props) { + if (event.category === 'VERSION') { + return ( + + {event.name} + + ); + } + + return ( +
    + {translate('event.category', event.category)}:{' '} + {event.description ? ( + + {event.name} + + ) : ( + {event.name} + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js deleted file mode 100644 index 7c6fdb452ad..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js +++ /dev/null @@ -1,35 +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 React from 'react'; -import { shallow } from 'enzyme'; -import Analysis from '../Analysis'; - -const ANALYSIS = { - key: '1', - date: '2017-06-10T16:10:59+0200', - events: [ - { key: '1', category: 'OTHER', name: 'test' }, - { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' } - ] -}; - -it('should sort the events with version first', () => { - expect(shallow()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.tsx b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.tsx new file mode 100644 index 00000000000..16c481b42db --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.tsx @@ -0,0 +1,35 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import Analysis from '../Analysis'; + +const ANALYSIS = { + key: '1', + date: '2017-06-10T16:10:59+0200', + events: [ + { key: '1', category: 'OTHER', name: 'test' }, + { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' } + ] +}; + +it('should sort the events with version first', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js deleted file mode 100644 index 496bf250aee..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js +++ /dev/null @@ -1,33 +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 React from 'react'; -import { shallow } from 'enzyme'; -import Event from '../Event'; - -const EVENT = { key: '1', category: 'OTHER', name: 'test' }; -const VERSION = { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' }; - -it('should render an event correctly', () => { - expect(shallow()).toMatchSnapshot(); -}); - -it('should render a version correctly', () => { - expect(shallow()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.tsx b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.tsx new file mode 100644 index 00000000000..e15be764817 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.tsx @@ -0,0 +1,33 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import Event from '../Event'; + +const EVENT = { key: '1', category: 'OTHER', name: 'test' }; +const VERSION = { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' }; + +it('should render an event correctly', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render a version correctly', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap deleted file mode 100644 index a9296187266..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should sort the events with version first 1`] = ` -
  • -
    - - - -
    -
    - - -
    -
  • -`; diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.tsx.snap new file mode 100644 index 00000000000..a9296187266 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should sort the events with version first 1`] = ` +
  • +
    + + + +
    +
    + + +
    +
  • +`; diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap deleted file mode 100644 index f6b5bdfc0e6..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render a version correctly 1`] = ` - - - 6.5-SNAPSHOT - - -`; - -exports[`should render an event correctly 1`] = ` -
    - - event.category.OTHER - : - - - - test - -
    -`; diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.tsx.snap new file mode 100644 index 00000000000..f6b5bdfc0e6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.tsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render a version correctly 1`] = ` + + + 6.5-SNAPSHOT + + +`; + +exports[`should render an event correctly 1`] = ` +
    + + event.category.OTHER + : + + + + test + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/overview/main/Coverage.js b/server/sonar-web/src/main/js/apps/overview/main/Coverage.js index d8f4a118c43..ecc08743527 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/Coverage.js +++ b/server/sonar-web/src/main/js/apps/overview/main/Coverage.js @@ -19,7 +19,7 @@ */ import React from 'react'; import enhance from './enhance'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import DrilldownLink from '../../../components/shared/DrilldownLink'; import { getMetricName } from '../helpers/metrics'; import { formatMeasure, getPeriodValue } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/overview/main/Duplications.js b/server/sonar-web/src/main/js/apps/overview/main/Duplications.js index 3a04c3cc306..e668ff8efc2 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/Duplications.js +++ b/server/sonar-web/src/main/js/apps/overview/main/Duplications.js @@ -19,7 +19,7 @@ */ import React from 'react'; import enhance from './enhance'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import DrilldownLink from '../../../components/shared/DrilldownLink'; import { getMetricName } from '../helpers/metrics'; import { formatMeasure, getPeriodValue } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js deleted file mode 100644 index 8aac3e3985b..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js +++ /dev/null @@ -1,219 +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 React from 'react'; -import { Link } from 'react-router'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; -import BubblesIcon from '../../../components/icons-components/BubblesIcon'; -import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; -import HistoryIcon from '../../../components/icons-components/HistoryIcon'; -import Rating from './../../../components/ui/Rating'; -import Timeline from '../components/Timeline'; -import Tooltip from '../../../components/controls/Tooltip'; -import { - formatMeasure, - formatMeasureVariation, - isDiffMetric, - getPeriodValue, - getShortType, - getRatingTooltip -} from '../../../helpers/measures'; -import { translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n'; -import { getPeriodDate } from '../../../helpers/periods'; -import { - getComponentDrilldownUrl, - getComponentIssuesUrl, - getMeasureHistoryUrl -} from '../../../helpers/urls'; - -export default function enhance(ComposedComponent) { - return class extends React.PureComponent { - static displayName = `enhance(${ComposedComponent.displayName})}`; - - getValue = measure => { - const { leakPeriod } = this.props; - - if (!measure) { - return 0; - } - - return isDiffMetric(measure.metric.key) - ? getPeriodValue(measure, leakPeriod.index) - : measure.value; - }; - - renderHeader = (domain, label) => { - const { branch, component } = this.props; - return ( -
    -
    - {label} - - - -
    -
    - ); - }; - - renderMeasure = metricKey => { - const { branch, measures, component } = this.props; - const measure = measures.find(measure => measure.metric.key === metricKey); - - if (measure == null) { - return null; - } - - return ( -
    -
    - - - {formatMeasure(measure.value, getShortType(measure.metric.type))} - - -
    - -
    - {getLocalizedMetricName(measure.metric)} - {this.renderHistoryLink(measure.metric.key)} -
    -
    - ); - }; - - renderMeasureVariation = (metricKey, customLabel) => { - const NO_VALUE = '—'; - const { measures, leakPeriod } = this.props; - const measure = measures.find(measure => measure.metric.key === metricKey); - const periodValue = getPeriodValue(measure, leakPeriod.index); - const formatted = - periodValue != null - ? formatMeasureVariation(periodValue, getShortType(measure.metric.type)) - : NO_VALUE; - return ( -
    -
    {formatted}
    - -
    {customLabel || measure.metric.name}
    -
    - ); - }; - - renderRating = metricKey => { - const { branch, component, measures } = this.props; - const measure = measures.find(measure => measure.metric.key === metricKey); - if (!measure) { - return null; - } - const value = this.getValue(measure); - const title = getRatingTooltip(metricKey, value); - return ( - -
    - - - -
    -
    - ); - }; - - renderIssues = (metric, type) => { - const { branch, measures, component } = this.props; - const measure = measures.find(measure => measure.metric.key === metric); - const value = this.getValue(measure); - const params = { branch, resolved: 'false', types: type }; - if (isDiffMetric(metric)) { - Object.assign(params, { sinceLeakPeriod: 'true' }); - } - - const tooltip = ( - - {formattedAnalysisDate => ( - - {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)} - - )} - - ); - - return ( - - - {formatMeasure(value, 'SHORT_INT')} - - - ); - }; - - renderHistoryLink = metricKey => { - const linkClass = 'button button-small spacer-left overview-domain-measure-history-link'; - return ( - - - - ); - }; - - renderTimeline = (metricKey, range, children) => { - if (!this.props.history) { - return null; - } - const history = this.props.history[metricKey]; - if (!history) { - return null; - } - const props = { - history, - [range]: getPeriodDate(this.props.leakPeriod) - }; - return ( -
    - - {children} -
    - ); - }; - - render() { - return ( - - ); - } - }; -} diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx b/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx new file mode 100644 index 00000000000..142cdf3eb90 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx @@ -0,0 +1,220 @@ +/* + * 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 * as React from 'react'; +import { Link } from 'react-router'; +import DrilldownLink from '../../../components/shared/DrilldownLink'; +import BubblesIcon from '../../../components/icons-components/BubblesIcon'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import HistoryIcon from '../../../components/icons-components/HistoryIcon'; +import Rating from './../../../components/ui/Rating'; +import Timeline from '../components/Timeline'; +import Tooltip from '../../../components/controls/Tooltip'; +import { + formatMeasure, + isDiffMetric, + getPeriodValue, + getShortType, + getRatingTooltip, + MeasureEnhanced +} from '../../../helpers/measures'; +import { translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n'; +import { getPeriodDate } from '../../../helpers/periods'; +import { + getComponentDrilldownUrl, + getComponentIssuesUrl, + getMeasureHistoryUrl +} from '../../../helpers/urls'; +import { Component } from '../../../app/types'; +import { History } from '../../../api/time-machine'; + +export interface EnhanceProps { + branch?: string; + component: Component; + measures: MeasureEnhanced[]; + leakPeriod?: { index: number; date?: string }; + history?: History; + historyStartDate?: Date; +} + +export interface ComposedProps extends EnhanceProps { + getValue: (measure: MeasureEnhanced) => string | undefined; + renderHeader: (domain: string, label: string) => React.ReactNode; + renderMeasure: (metricKey: string) => React.ReactNode; + renderRating: (metricKey: string) => React.ReactNode; + renderIssues: (metric: string, type: string) => React.ReactNode; + renderHistoryLink: (metricKey: string) => React.ReactNode; + renderTimeline: (metricKey: string, range: string, children?: React.ReactNode) => React.ReactNode; +} + +export default function enhance(ComposedComponent: React.ComponentType) { + return class extends React.PureComponent { + static displayName = `enhance(${ComposedComponent.displayName})}`; + + getValue = (measure: MeasureEnhanced) => { + const { leakPeriod } = this.props; + if (!measure) { + return '0'; + } + return isDiffMetric(measure.metric.key) + ? getPeriodValue(measure, leakPeriod ? leakPeriod.index : 0) + : measure.value; + }; + + renderHeader = (domain: string, label: string) => { + const { branch, component } = this.props; + return ( +
    +
    + {label} + + + +
    +
    + ); + }; + + renderMeasure = (metricKey: string) => { + const { branch, measures, component } = this.props; + const measure = measures.find(measure => measure.metric.key === metricKey); + if (!measure) { + return null; + } + + return ( +
    +
    + + + {formatMeasure(measure.value, getShortType(measure.metric.type))} + + +
    + +
    + {getLocalizedMetricName(measure.metric)} + {this.renderHistoryLink(measure.metric.key)} +
    +
    + ); + }; + + renderRating = (metricKey: string) => { + const { branch, component, measures } = this.props; + const measure = measures.find(measure => measure.metric.key === metricKey); + if (!measure) { + return null; + } + + const value = this.getValue(measure); + const title = value && getRatingTooltip(metricKey, value); + return ( + +
    + + + +
    +
    + ); + }; + + renderIssues = (metric: string, type: string) => { + const { branch, measures, component } = this.props; + const measure = measures.find(measure => measure.metric.key === metric); + if (!measure) { + return null; + } + + const value = this.getValue(measure); + const params = { branch, resolved: 'false', types: type }; + if (isDiffMetric(metric)) { + Object.assign(params, { sinceLeakPeriod: 'true' }); + } + + const tooltip = component.analysisDate && ( + + {formattedAnalysisDate => ( + + {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)} + + )} + + ); + + return ( + + + {formatMeasure(value, 'SHORT_INT')} + + + ); + }; + + renderHistoryLink = (metricKey: string) => { + const linkClass = 'button button-small spacer-left overview-domain-measure-history-link'; + return ( + + + + ); + }; + + renderTimeline = (metricKey: string, range: 'before' | 'after', children?: React.ReactNode) => { + if (!this.props.history) { + return null; + } + const history = this.props.history[metricKey]; + if (!history) { + return null; + } + const props = { history, [range]: getPeriodDate(this.props.leakPeriod) }; + return ( +
    + + {children} +
    + ); + }; + + render() { + return ( + + ); + } + }; +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js deleted file mode 100644 index 0422440aa99..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ /dev/null @@ -1,109 +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 React from 'react'; -import { connect } from 'react-redux'; -import MetaKey from './MetaKey'; -import MetaOrganizationKey from './MetaOrganizationKey'; -import MetaLinks from './MetaLinks'; -import MetaQualityGate from './MetaQualityGate'; -import MetaQualityProfiles from './MetaQualityProfiles'; -import AnalysesList from '../events/AnalysesList'; -import MetaSize from './MetaSize'; -import MetaTags from './MetaTags'; -import BadgesModal from '../badges/BadgesModal'; -import { areThereCustomOrganizations, getGlobalSettingValue } from '../../../store/rootReducer'; -import { Visibility } from '../../../app/types'; - -const Meta = ({ - branch, - component, - history, - measures, - areThereCustomOrganizations, - onComponentChange, - onSonarCloud -}) => { - const { qualifier, description, qualityProfiles, qualityGate, visibility } = component; - - const isProject = qualifier === 'TRK'; - const isPrivate = visibility === Visibility.Private; - - const hasDescription = !!description; - const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0; - const hasQualityGate = !!qualityGate; - - const shouldShowQualityProfiles = isProject && hasQualityProfiles; - const shouldShowQualityGate = isProject && hasQualityGate; - const hasOrganization = component.organization != null && areThereCustomOrganizations; - - return ( -
    - {hasDescription && ( -
    {description}
    - )} - - - - {isProject && } - - - - {shouldShowQualityGate && ( - - )} - - {shouldShowQualityProfiles && ( - - )} - - {isProject && } - - - - {hasOrganization && } - - {onSonarCloud && - isProject && - !isPrivate && } -
    - ); -}; - -const mapStateToProps = state => { - const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled'); - return { - areThereCustomOrganizations: areThereCustomOrganizations(state), - onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true') - }; -}; - -export default connect(mapStateToProps)(Meta); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx new file mode 100644 index 00000000000..bfffecd1f7c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx @@ -0,0 +1,117 @@ +/* + * 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 * as React from 'react'; +import { connect } from 'react-redux'; +import MetaKey from './MetaKey'; +import MetaOrganizationKey from './MetaOrganizationKey'; +import MetaLinks from './MetaLinks'; +import MetaQualityGate from './MetaQualityGate'; +import MetaQualityProfiles from './MetaQualityProfiles'; +import AnalysesList from '../events/AnalysesList'; +import MetaSize from './MetaSize'; +import MetaTags from './MetaTags'; +import BadgesModal from '../badges/BadgesModal'; +import { areThereCustomOrganizations, getGlobalSettingValue } from '../../../store/rootReducer'; +import { Visibility, Component } from '../../../app/types'; +import { History } from '../../../api/time-machine'; +import { MeasureEnhanced } from '../../../helpers/measures'; + +interface OwnProps { + branch?: string; + component: Component; + history?: History; + measures: MeasureEnhanced[]; + onComponentChange: (changes: {}) => void; +} + +interface StateToProps { + areThereCustomOrganizations: boolean; + onSonarCloud: boolean; +} + +export function Meta(props: OwnProps & StateToProps) { + const { branch, component, areThereCustomOrganizations } = props; + const { qualifier, description, qualityProfiles, qualityGate, visibility } = component; + + const isProject = qualifier === 'TRK'; + const isPrivate = visibility === Visibility.Private; + + const hasDescription = !!description; + const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0; + const hasQualityGate = !!qualityGate; + + const shouldShowQualityProfiles = isProject && hasQualityProfiles; + const shouldShowQualityGate = isProject && hasQualityGate; + const hasOrganization = component.organization != null && areThereCustomOrganizations; + + return ( +
    + {hasDescription && ( +
    {description}
    + )} + + + + {isProject && } + + + + {shouldShowQualityGate && ( + + )} + + {shouldShowQualityProfiles && ( + + )} + + {isProject && } + + + + {hasOrganization && } + + {props.onSonarCloud && + isProject && + !isPrivate && } +
    + ); +} + +const mapStateToProps = (state: any): StateToProps => { + const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled'); + return { + areThereCustomOrganizations: areThereCustomOrganizations(state), + onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true') + }; +}; + +export default connect(mapStateToProps)(Meta); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.js deleted file mode 100644 index a59e818bc16..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.js +++ /dev/null @@ -1,96 +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. - */ -// @flow -import React from 'react'; -import { isProvided, isClickable } from '../../project-admin/links/utils'; -import BugTrackerIcon from '../../../components/ui/BugTrackerIcon'; - -/*:: -type Link = { - id: string, - name: string, - url: string, - type: string -}; -*/ - -/*:: -type State = {| - expanded: boolean -|}; -*/ - -export default class MetaLink extends React.PureComponent { - /*:: props: { - link: Link - }; -*/ - - state /*: State */ = { - expanded: false - }; - - handleClick = (e /*: Object */) => { - e.preventDefault(); - e.target.blur(); - this.setState((s /*: State */) => ({ expanded: !s.expanded })); - }; - - renderLinkIcon(link /*: Link */) { - if (link.type === 'issue') { - return ; - } - - return isProvided(link) ? ( - - ) : ( - - ); - } - - render() { - const { link } = this.props; - - return ( -
  • - - {this.renderLinkIcon(link)} -   - {link.name} - - {this.state.expanded && ( -
    - e.target.select()} - /> -
    - )} -
  • - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx new file mode 100644 index 00000000000..b704420b5ed --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx @@ -0,0 +1,86 @@ +/* + * 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 * as React from 'react'; +import { isProvided, isClickable } from '../../project-admin/links/utils'; +import BugTrackerIcon from '../../../components/ui/BugTrackerIcon'; +import { ProjectLink } from '../../../api/projectLinks'; + +interface Props { + link: ProjectLink; +} + +interface State { + expanded: boolean; +} + +export default class MetaLink extends React.PureComponent { + state: State = { expanded: false }; + + handleClick = (e: React.SyntheticEvent) => { + e.preventDefault(); + e.currentTarget.blur(); + this.setState((s: State) => ({ expanded: !s.expanded })); + }; + + handleInputClick = (e: React.SyntheticEvent) => { + e.currentTarget.select(); + }; + + renderLinkIcon = (link: ProjectLink) => { + if (link.type === 'issue') { + return ; + } + + return isProvided(link) ? ( + + ) : ( + + ); + }; + + render() { + const { link } = this.props; + + return ( +
  • + + {this.renderLinkIcon(link)} +   + {link.name} + + {this.state.expanded && ( +
    + +
    + )} +
  • + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js deleted file mode 100644 index 7e2ca4c1761..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js +++ /dev/null @@ -1,73 +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 React from 'react'; -import PropTypes from 'prop-types'; -import MetaLink from './MetaLink'; -import { getProjectLinks } from '../../../api/projectLinks'; -import { orderLinks } from '../../project-admin/links/utils'; - -export default class MetaLinks extends React.PureComponent { - static propTypes = { - component: PropTypes.object.isRequired - }; - - state = {}; - - componentDidMount() { - this.mounted = true; - this.loadLinks(); - } - - componentDidUpdate(prevProps) { - if (prevProps.component.key !== this.props.component.key) { - this.loadLinks(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - loadLinks() { - getProjectLinks(this.props.component.key).then(links => { - if (this.mounted) { - this.setState({ links }); - } - }); - } - - render() { - const { links } = this.state; - - if (links == null || links.length === 0) { - return null; - } - - const orderedLinks = orderLinks(links); - - return ( -
    -
      - {orderedLinks.map(link => )} -
    -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx new file mode 100644 index 00000000000..66980dda926 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx @@ -0,0 +1,80 @@ +/* + * 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 * as React from 'react'; +import MetaLink from './MetaLink'; +import { getProjectLinks, ProjectLink } from '../../../api/projectLinks'; +import { orderLinks } from '../../project-admin/links/utils'; +import { LightComponent } from '../../../app/types'; + +interface Props { + component: LightComponent; +} + +interface State { + links?: ProjectLink[]; +} + +export default class MetaLinks extends React.PureComponent { + mounted: boolean; + state: State = {}; + + componentDidMount() { + this.mounted = true; + this.loadLinks(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.component.key !== this.props.component.key) { + this.loadLinks(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + loadLinks = () => + getProjectLinks(this.props.component.key).then( + links => { + if (this.mounted) { + this.setState({ links }); + } + }, + () => {} + ); + + render() { + const { links } = this.state; + + if (!links || links.length === 0) { + return null; + } + + const orderedLinks = orderLinks(links); + + return ( +
    +
      + {orderedLinks.map(link => )} +
    +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js deleted file mode 100644 index 9d34ec90c2c..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js +++ /dev/null @@ -1,99 +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 React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; -import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer'; -import SizeRating from '../../../components/ui/SizeRating'; -import { formatMeasure } from '../../../helpers/measures'; -import { getMetricName } from '../helpers/metrics'; -import { translate } from '../../../helpers/l10n'; - -export default class MetaSize extends React.PureComponent { - static propTypes = { - branch: PropTypes.string, - component: PropTypes.object.isRequired, - measures: PropTypes.array.isRequired - }; - - renderLoC = ncloc => ( -
    - - - - - {formatMeasure(ncloc.value, 'SHORT_INT')} - -
    {getMetricName('ncloc')}
    -
    - ); - - renderLoCDistribution = () => { - const languageDistribution = this.props.measures.find( - measure => measure.metric.key === 'ncloc_language_distribution' - ); - - const className = - this.props.component.qualifier === 'TRK' ? 'overview-meta-size-lang-dist' : 'big-spacer-top'; - - return languageDistribution ? ( -
    - -
    - ) : null; - }; - - renderProjects = () => { - const projects = this.props.measures.find(measure => measure.metric.key === 'projects'); - - return projects ? ( -
    - - {formatMeasure(projects.value, 'SHORT_INT')} - -
    {translate('metric.projects.name')}
    -
    - ) : null; - }; - - render() { - const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc'); - - if (ncloc == null) { - return null; - } - - return ( -
    - {this.props.component.qualifier === 'APP' && this.renderProjects()} - {this.renderLoC(ncloc)} - {this.renderLoCDistribution()} -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx new file mode 100644 index 00000000000..6465c861817 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx @@ -0,0 +1,99 @@ +/* + * 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 * as React from 'react'; +import * as classNames from 'classnames'; +import DrilldownLink from '../../../components/shared/DrilldownLink'; +import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer'; +import SizeRating from '../../../components/ui/SizeRating'; +import { formatMeasure, MeasureEnhanced } from '../../../helpers/measures'; +import { getMetricName } from '../helpers/metrics'; +import { translate } from '../../../helpers/l10n'; +import { LightComponent } from '../../../app/types'; + +interface Props { + branch?: string; + component: LightComponent; + measures: MeasureEnhanced[]; +} + +export default class MetaSize extends React.PureComponent { + renderLoC = (ncloc: MeasureEnhanced) => ( +
    + + + + + {formatMeasure(ncloc.value, 'SHORT_INT')} + +
    {getMetricName('ncloc')}
    +
    + ); + + renderLoCDistribution = () => { + const languageDistribution = this.props.measures.find( + measure => measure.metric.key === 'ncloc_language_distribution' + ); + + const className = + this.props.component.qualifier === 'TRK' ? 'overview-meta-size-lang-dist' : 'big-spacer-top'; + + return languageDistribution ? ( +
    + +
    + ) : null; + }; + + renderProjects = () => { + const projects = this.props.measures.find(measure => measure.metric.key === 'projects'); + + return projects ? ( +
    + + {formatMeasure(projects.value, 'SHORT_INT')} + +
    {translate('metric.projects.name')}
    +
    + ) : null; + }; + + render() { + const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc'); + + if (ncloc == null) { + return null; + } + + return ( +
    + {this.props.component.qualifier === 'APP' && this.renderProjects()} + {this.renderLoC(ncloc)} + {this.renderLoCDistribution()} +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js deleted file mode 100644 index 409f3049e9a..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js +++ /dev/null @@ -1,149 +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. - */ -//@flow -import React from 'react'; -import { setProjectTags } from '../../../api/components'; -import { translate } from '../../../helpers/l10n'; -import TagsList from '../../../components/tags/TagsList'; -import MetaTagsSelector from './MetaTagsSelector'; - -/*:: -type Props = { - component: { - key: string, - tags: Array, - configuration?: { - showSettings?: boolean - } - }, - onComponentChange: {} => void -}; -*/ - -/*:: -type State = { - popupOpen: boolean, - popupPosition: { top: number, right: number } -}; -*/ - -export default class MetaTags extends React.PureComponent { - /*:: card: HTMLElement; */ - /*:: tagsList: HTMLElement; */ - /*:: tagsSelector: HTMLElement; */ - /*:: props: Props; */ - state /*: State */ = { - popupOpen: false, - popupPosition: { - top: 0, - right: 0 - } - }; - - componentDidMount() { - if (this.canUpdateTags()) { - const buttonPos = this.tagsList.getBoundingClientRect(); - const cardPos = this.card.getBoundingClientRect(); - this.setState({ popupPosition: this.getPopupPos(buttonPos, cardPos) }); - - window.addEventListener('keydown', this.handleKey, false); - window.addEventListener('click', this.handleOutsideClick, false); - } - } - - componentWillUnmount() { - window.removeEventListener('keydown', this.handleKey); - window.removeEventListener('click', this.handleOutsideClick); - } - - handleKey = (evt /*: KeyboardEvent */) => { - // Escape key - if (evt.keyCode === 27) { - this.setState({ popupOpen: false }); - } - }; - - handleOutsideClick = (evt /*: SyntheticInputEvent */) => { - if (!this.tagsSelector || !this.tagsSelector.contains(evt.target)) { - this.setState({ popupOpen: false }); - } - }; - - handleClick = (evt /*: MouseEvent */) => { - evt.stopPropagation(); - this.setState(state => ({ popupOpen: !state.popupOpen })); - }; - - canUpdateTags() { - const { configuration } = this.props.component; - return configuration && configuration.showSettings; - } - - getPopupPos( - eltPos /*: { height: number, width: number } */, - containerPos /*: { width: number } */ - ) { - return { - top: eltPos.height, - right: containerPos.width - eltPos.width - }; - } - - handleSetProjectTags = (tags /*: Array */) => { - setProjectTags({ project: this.props.component.key, tags: tags.join(',') }).then( - () => this.props.onComponentChange({ tags }), - () => {} - ); - }; - - render() { - const { tags, key } = this.props.component; - const { popupOpen, popupPosition } = this.state; - - if (this.canUpdateTags()) { - return ( -
    (this.card = card)}> - - {popupOpen && ( -
    (this.tagsSelector = tagsSelector)}> - -
    - )} -
    - ); - } else { - return ( -
    - -
    - ); - } - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx new file mode 100644 index 00000000000..f08c08252fe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx @@ -0,0 +1,129 @@ +/* + * 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 * as React from 'react'; +import { setProjectTags } from '../../../api/components'; +import { translate } from '../../../helpers/l10n'; +import TagsList from '../../../components/tags/TagsList'; +import MetaTagsSelector from './MetaTagsSelector'; +import { BubblePopupPosition } from '../../../components/common/BubblePopup'; +import { Component } from '../../../app/types'; + +interface Props { + component: Component; + onComponentChange: (changes: {}) => void; +} + +interface State { + popupOpen: boolean; + popupPosition: BubblePopupPosition; +} + +export default class MetaTags extends React.PureComponent { + card: HTMLDivElement | null; + tagsList: HTMLButtonElement | null; + tagsSelector: HTMLDivElement | null; + state: State = { popupOpen: false, popupPosition: { top: 0, right: 0 } }; + + componentDidMount() { + if (this.canUpdateTags() && this.tagsList && this.card) { + const buttonPos = this.tagsList.getBoundingClientRect(); + const cardPos = this.card.getBoundingClientRect(); + this.setState({ popupPosition: this.getPopupPos(buttonPos, cardPos) }); + + window.addEventListener('keydown', this.handleKey, false); + window.addEventListener('click', this.handleOutsideClick, false); + } + } + + componentWillUnmount() { + window.removeEventListener('keydown', this.handleKey); + window.removeEventListener('click', this.handleOutsideClick); + } + + handleKey = (evt: KeyboardEvent) => { + // Escape key + if (evt.keyCode === 27) { + this.setState({ popupOpen: false }); + } + }; + + handleOutsideClick = (evt: Event) => { + if (!this.tagsSelector || !this.tagsSelector.contains(evt.target as Node)) { + this.setState({ popupOpen: false }); + } + }; + + handleClick = (evt: React.SyntheticEvent) => { + evt.stopPropagation(); + this.setState(state => ({ popupOpen: !state.popupOpen })); + }; + + canUpdateTags = () => { + const { configuration } = this.props.component; + return configuration && configuration.showSettings; + }; + + getPopupPos = (eltPos: ClientRect, containerPos: ClientRect) => ({ + top: eltPos.height, + right: containerPos.width - eltPos.width + }); + + handleSetProjectTags = (tags: string[]) => { + setProjectTags({ project: this.props.component.key, tags: tags.join(',') }).then( + () => this.props.onComponentChange({ tags }), + () => {} + ); + }; + + render() { + const { key } = this.props.component; + const { popupOpen, popupPosition } = this.state; + const tags = this.props.component.tags || []; + + if (this.canUpdateTags()) { + return ( +
    (this.card = card)}> + + {popupOpen && ( +
    (this.tagsSelector = tagsSelector)}> + +
    + )} +
    + ); + } else { + return ( +
    + +
    + ); + } + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js deleted file mode 100644 index f728a15225d..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js +++ /dev/null @@ -1,81 +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. - */ -//@flow -import React from 'react'; -import { without } from 'lodash'; -import TagsSelector from '../../../components/tags/TagsSelector'; -import { searchProjectTags } from '../../../api/components'; - -/*:: -type Props = { - position: {}, - project: string, - selectedTags: Array, - setProjectTags: (Array) => void -}; -*/ - -/*:: -type State = { - searchResult: Array -}; -*/ - -const LIST_SIZE = 10; - -export default class MetaTagsSelector extends React.PureComponent { - /*:: props: Props; */ - state /*: State */ = { searchResult: [] }; - - componentDidMount() { - this.onSearch(''); - } - - onSearch = (query /*: string */) => { - searchProjectTags({ - q: query, - ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100) - }).then(result => { - this.setState({ searchResult: result.tags }); - }); - }; - - onSelect = (tag /*: string */) => { - this.props.setProjectTags([...this.props.selectedTags, tag]); - }; - - onUnselect = (tag /*: string */) => { - this.props.setProjectTags(without(this.props.selectedTags, tag)); - }; - - render() { - return ( - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx new file mode 100644 index 00000000000..2e2bb774a80 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx @@ -0,0 +1,74 @@ +/* + * 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 * as React from 'react'; +import { without } from 'lodash'; +import TagsSelector from '../../../components/tags/TagsSelector'; +import { BubblePopupPosition } from '../../../components/common/BubblePopup'; +import { searchProjectTags } from '../../../api/components'; + +interface Props { + position: BubblePopupPosition; + project: string; + selectedTags: string[]; + setProjectTags: (tags: string[]) => void; +} + +interface State { + searchResult: string[]; +} + +const LIST_SIZE = 10; + +export default class MetaTagsSelector extends React.PureComponent { + state: State = { searchResult: [] }; + + componentDidMount() { + this.onSearch(''); + } + + onSearch = (query: string) => { + searchProjectTags({ + q: query, + ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100) + }).then(result => this.setState({ searchResult: result.tags }), () => {}); + }; + + onSelect = (tag: string) => { + this.props.setProjectTags([...this.props.selectedTags, tag]); + }; + + onUnselect = (tag: string) => { + this.props.setProjectTags(without(this.props.selectedTags, tag)); + }; + + render() { + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.js b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.js deleted file mode 100644 index 2499c2e3c36..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.js +++ /dev/null @@ -1,54 +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 React from 'react'; -import { shallow } from 'enzyme'; -import MetaLink from '../MetaLink'; -import { click } from '../../../../helpers/testUtils'; - -it('should match snapshot', () => { - const link = { - id: '1', - name: 'Foo', - url: 'http://example.com', - type: 'foo' - }; - - expect(shallow()).toMatchSnapshot(); -}); - -it('should expand and collapse link', () => { - const link = { - id: '1', - name: 'Foo', - url: 'scm:git:git@github.com', - type: 'foo' - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - - // expand - click(wrapper.find('a')); - expect(wrapper).toMatchSnapshot(); - - // collapse - click(wrapper.find('a')); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx new file mode 100644 index 00000000000..0bea70841d8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx @@ -0,0 +1,54 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import MetaLink from '../MetaLink'; +import { click } from '../../../../helpers/testUtils'; + +it('should match snapshot', () => { + const link = { + id: '1', + name: 'Foo', + url: 'http://example.com', + type: 'foo' + }; + + expect(shallow()).toMatchSnapshot(); +}); + +it('should expand and collapse link', () => { + const link = { + id: '1', + name: 'Foo', + url: 'scm:git:git@github.com', + type: 'foo' + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + + // expand + click(wrapper.find('a')); + expect(wrapper).toMatchSnapshot(); + + // collapse + click(wrapper.find('a')); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.js b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.js deleted file mode 100644 index 9a621ce7aa9..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.js +++ /dev/null @@ -1,66 +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 React from 'react'; -import { shallow } from 'enzyme'; -import { click } from '../../../../helpers/testUtils'; -import MetaTags from '../MetaTags'; - -const component = { - key: 'my-project', - tags: [], - configuration: { - showSettings: false - } -}; - -const componentWithTags = { - key: 'my-second-project', - tags: ['foo', 'bar'], - configuration: { - showSettings: true - } -}; - -it('should render without tags and admin rights', () => { - expect( - shallow(, { disableLifecycleMethods: true }) - ).toMatchSnapshot(); -}); - -it('should render with tags and admin rights', () => { - expect( - shallow(, { disableLifecycleMethods: true }) - ).toMatchSnapshot(); -}); - -it('should open the tag selector on click', () => { - const wrapper = shallow(, { - disableLifecycleMethods: true - }); - expect(wrapper).toMatchSnapshot(); - - // open - click(wrapper.find('button')); - expect(wrapper).toMatchSnapshot(); - - // close - click(wrapper.find('button')); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx new file mode 100644 index 00000000000..1479225a022 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTags-test.tsx @@ -0,0 +1,81 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import MetaTags from '../MetaTags'; + +const component = { + key: 'my-project', + tags: [], + configuration: { + showSettings: false + }, + organization: 'foo', + qualifier: 'TRK', + name: 'MyProject', + breadcrumbs: [] +}; + +const componentWithTags = { + key: 'my-second-project', + tags: ['foo', 'bar'], + configuration: { + showSettings: true + }, + organization: 'foo', + qualifier: 'TRK', + name: 'MySecondProject', + breadcrumbs: [] +}; + +it('should render without tags and admin rights', () => { + expect( + shallow(, { + disableLifecycleMethods: true + }) + ).toMatchSnapshot(); +}); + +it('should render with tags and admin rights', () => { + expect( + shallow(, { + disableLifecycleMethods: true + }) + ).toMatchSnapshot(); +}); + +it('should open the tag selector on click', () => { + const wrapper = shallow( + , + { + disableLifecycleMethods: true + } + ); + expect(wrapper).toMatchSnapshot(); + + // open + click(wrapper.find('button')); + expect(wrapper).toMatchSnapshot(); + + // close + click(wrapper.find('button')); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js deleted file mode 100644 index 2f7a364d101..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js +++ /dev/null @@ -1,62 +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. - */ -/* eslint-disable import/order, import/first */ -import React from 'react'; -import { mount, shallow } from 'enzyme'; -import MetaTagsSelector from '../MetaTagsSelector'; - -jest.mock('../../../../api/components', () => ({ - searchProjectTags: jest.fn() -})); - -jest.mock('lodash', () => { - const lodash = require.requireActual('lodash'); - lodash.debounce = jest.fn(fn => fn); - return lodash; -}); - -import { searchProjectTags } from '../../../../api/components'; - -it('searches tags on mount', () => { - searchProjectTags.mockImplementation(() => Promise.resolve({ tags: ['foo', 'bar'] })); - mount( - - ); - expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' }); -}); - -it('selects and deselects tags', () => { - const setProjectTags = jest.fn(); - const wrapper = shallow( - - ); - - wrapper.find('TagsSelector').prop('onSelect')('baz'); - expect(setProjectTags).toHaveBeenLastCalledWith(['foo', 'bar', 'baz']); - - // note that the `selectedTags` is a prop and so it wasn't changed - wrapper.find('TagsSelector').prop('onUnselect')('bar'); - expect(setProjectTags).toHaveBeenLastCalledWith(['foo']); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx new file mode 100644 index 00000000000..aefeb8d6fb6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.tsx @@ -0,0 +1,70 @@ +/* + * 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. + */ +/* eslint-disable import/order, import/first */ +import * as React from 'react'; +import { mount, shallow } from 'enzyme'; +import MetaTagsSelector from '../MetaTagsSelector'; + +jest.mock('../../../../api/components', () => ({ + searchProjectTags: jest.fn() +})); + +jest.mock('lodash', () => { + const lodash = require.requireActual('lodash'); + lodash.debounce = jest.fn(fn => fn); + return lodash; +}); + +import { searchProjectTags } from '../../../../api/components'; + +it('searches tags on mount', () => { + (searchProjectTags as jest.Mock).mockImplementation(() => + Promise.resolve({ tags: ['foo', 'bar'] }) + ); + mount( + + ); + expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' }); +}); + +it('selects and deselects tags', () => { + const setProjectTags = jest.fn(); + const wrapper = shallow( + + ); + + const tagSelect: any = wrapper.find('TagsSelector'); + tagSelect.prop('onSelect')('baz'); + expect(setProjectTags).toHaveBeenLastCalledWith(['foo', 'bar', 'baz']); + + // note that the `selectedTags` is a prop and so it wasn't changed + tagSelect.prop('onUnselect')('bar'); + expect(setProjectTags).toHaveBeenLastCalledWith(['foo']); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.js.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.js.snap deleted file mode 100644 index 20c6a30481a..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.js.snap +++ /dev/null @@ -1,79 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should expand and collapse link 1`] = ` -
  • - - -   - Foo - -
  • -`; - -exports[`should expand and collapse link 2`] = ` -
  • - - -   - Foo - -
    - -
    -
  • -`; - -exports[`should expand and collapse link 3`] = ` -
  • - - -   - Foo - -
  • -`; - -exports[`should match snapshot 1`] = ` -
  • - - -   - Foo - -
  • -`; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap new file mode 100644 index 00000000000..20c6a30481a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should expand and collapse link 1`] = ` +
  • + + +   + Foo + +
  • +`; + +exports[`should expand and collapse link 2`] = ` +
  • + + +   + Foo + +
    + +
    +
  • +`; + +exports[`should expand and collapse link 3`] = ` +
  • + + +   + Foo + +
  • +`; + +exports[`should match snapshot 1`] = ` +
  • + + +   + Foo + +
  • +`; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap deleted file mode 100644 index 875302462d5..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap +++ /dev/null @@ -1,118 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should open the tag selector on click 1`] = ` -
    - -
    -`; - -exports[`should open the tag selector on click 2`] = ` -
    - -
    - -
    -
    -`; - -exports[`should open the tag selector on click 3`] = ` -
    - -
    -`; - -exports[`should render with tags and admin rights 1`] = ` -
    - -
    -`; - -exports[`should render without tags and admin rights 1`] = ` -
    - -
    -`; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap new file mode 100644 index 00000000000..875302462d5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should open the tag selector on click 1`] = ` +
    + +
    +`; + +exports[`should open the tag selector on click 2`] = ` +
    + +
    + +
    +
    +`; + +exports[`should open the tag selector on click 3`] = ` +
    + +
    +`; + +exports[`should render with tags and admin rights 1`] = ` +
    + +
    +`; + +exports[`should render without tags and admin rights 1`] = ` +
    + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js deleted file mode 100644 index 6528d5a44e8..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.js +++ /dev/null @@ -1,124 +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. - */ -// @flow -import React from 'react'; -import { keyBy } from 'lodash'; -import ApplicationQualityGateProject from './ApplicationQualityGateProject'; -import Level from '../../../components/ui/Level'; -import { getApplicationQualityGate } from '../../../api/quality-gates'; -import { translate } from '../../../helpers/l10n'; - -/*:: -type Props = { - component: { key: string, organization?: string } -}; -*/ - -/*:: -type State = { - loading: boolean, - metrics?: { [string]: Object }, - projects?: Array<{ - conditions: Array, - key: string, - name: string, - status: string - }>, - status?: string -}; -*/ - -export default class ApplicationQualityGate extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: props: Props; */ - state /*: State */ = { - loading: true - }; - - componentDidMount() { - this.mounted = true; - this.fetchDetails(); - } - - componentDidUpdate(prevProps /*: Props */) { - if (prevProps.component.key !== this.props.component.key) { - this.fetchDetails(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - fetchDetails = () => { - const { component } = this.props; - this.setState({ loading: true }); - getApplicationQualityGate({ - application: component.key, - organization: component.organization - }).then( - ({ status, projects, metrics }) => { - if (this.mounted) { - this.setState({ - loading: false, - metrics: keyBy(metrics, 'key'), - status, - projects - }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - }; - - render() { - const { metrics, status, projects } = this.state; - - return ( -
    -

    - {translate('overview.quality_gate')} - {this.state.loading && } - {status != null && } -

    - - {projects != null && ( -
    - {projects - .filter(project => project.status !== 'OK') - .map(project => ( - - ))} -
    - )} -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx new file mode 100644 index 00000000000..3d8202f0d9d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx @@ -0,0 +1,113 @@ +/* + * 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 * as React from 'react'; +import { keyBy } from 'lodash'; +import ApplicationQualityGateProject from './ApplicationQualityGateProject'; +import Level from '../../../components/ui/Level'; +import { getApplicationQualityGate, ApplicationProject } from '../../../api/quality-gates'; +import { translate } from '../../../helpers/l10n'; +import { LightComponent, Metric } from '../../../app/types'; + +interface Props { + component: LightComponent; +} + +type State = { + loading: boolean; + metrics?: { [key: string]: Metric }; + projects?: ApplicationProject[]; + status?: string; +}; + +export default class ApplicationQualityGate extends React.PureComponent { + mounted: boolean; + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + this.fetchDetails(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.component.key !== this.props.component.key) { + this.fetchDetails(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchDetails = () => { + const { component } = this.props; + this.setState({ loading: true }); + getApplicationQualityGate({ + application: component.key, + organization: component.organization + }).then( + ({ status, projects, metrics }) => { + if (this.mounted) { + this.setState({ + loading: false, + metrics: keyBy(metrics, 'key'), + status, + projects + }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + render() { + const { metrics, status, projects } = this.state; + + return ( +
    +

    + {translate('overview.quality_gate')} + {this.state.loading && } + {status != null && } +

    + + {projects && + metrics && ( +
    + {projects + .filter(project => project.status !== 'OK') + .map(project => ( + + ))} +
    + )} +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js deleted file mode 100644 index 451c808abc9..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.js +++ /dev/null @@ -1,105 +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. - */ -// @flow -import React from 'react'; -import { Link } from 'react-router'; -import classNames from 'classnames'; -import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; -import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; -import { getProjectUrl } from '../../../helpers/urls'; -import './ApplicationQualityGateProject.css'; - -/*:: -type Condition = { - comparator: string, - errorThreshold?: string, - metric: string, - onLeak: boolean, - status: string, - value: string, - warningThreshold?: string -}; -*/ - -/*:: -type Props = { - metrics: { - [string]: { - key: string, - name: string, - type: string - } - }, - project: { - conditions: Array, - key: string, - name: string, - status: string - } -}; -*/ - -export default class ApplicationQualityGateProject extends React.PureComponent { - /*:: props: Props; */ - - renderCondition = (condition /*: Condition */) => { - const metric = this.props.metrics[condition.metric]; - const metricName = getLocalizedMetricName(metric); - const threshold = condition.errorThreshold || condition.warningThreshold; - const isDiff = isDiffMetric(condition.metric); - - return ( -
  • - - {formatMeasure(condition.value, metric.type)} {metricName} - {!isDiff && condition.onLeak && ' ' + translate('quality_gates.conditions.leak')} - - - {translate('quality_gates.operator', condition.comparator, 'short')}{' '} - {formatMeasure(threshold, metric.type)} - -
  • - ); - }; - - render() { - const { project } = this.props; - - return ( - -
    -

    {project.name}

    -
      - {project.conditions.filter(c => c.status !== 'OK').map(this.renderCondition)} -
    -
    - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.tsx new file mode 100644 index 00000000000..388b77005ee --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGateProject.tsx @@ -0,0 +1,79 @@ +/* + * 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 * as React from 'react'; +import * as classNames from 'classnames'; +import { Link } from 'react-router'; +import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; +import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; +import { getProjectUrl } from '../../../helpers/urls'; +import './ApplicationQualityGateProject.css'; +import { Metric } from '../../../app/types'; +import { ApplicationProject, ConditionAnalysis } from '../../../api/quality-gates'; + +interface Props { + metrics: { [key: string]: Metric }; + project: ApplicationProject; +} + +export default class ApplicationQualityGateProject extends React.PureComponent { + renderCondition = (condition: ConditionAnalysis) => { + const metric = this.props.metrics[condition.metric]; + const metricName = getLocalizedMetricName(metric); + const threshold = condition.errorThreshold || condition.warningThreshold; + const isDiff = isDiffMetric(condition.metric); + + return ( +
  • + + {formatMeasure(condition.value, metric.type)} {metricName} + {!isDiff && condition.onLeak && ' ' + translate('quality_gates.conditions.leak')} + + + {translate('quality_gates.operator', condition.comparator, 'short')}{' '} + {formatMeasure(threshold, metric.type)} + +
  • + ); + }; + + render() { + const { project } = this.props; + + return ( + +
    +

    {project.name}

    +
      + {project.conditions.filter(c => c.status !== 'OK').map(this.renderCondition)} +
    +
    + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js index 71eeccc2932..3301c33ef6f 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js @@ -21,7 +21,7 @@ import React from 'react'; import classNames from 'classnames'; import { Link } from 'react-router'; -import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import DrilldownLink from '../../../components/shared/DrilldownLink'; import Measure from '../../../components/measure/Measure'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import { getPeriodValue, isDiffMetric, formatMeasure } from '../../../helpers/measures'; @@ -146,7 +146,12 @@ export default class QualityGateCondition extends React.PureComponent { return this.wrapWithLink(
    - +
    diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js deleted file mode 100644 index 5d6170c69ea..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.js +++ /dev/null @@ -1,39 +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. - */ -// @flow -import React from 'react'; -import { shallow } from 'enzyme'; -import ApplicationQualityGate from '../ApplicationQualityGate'; - -it('renders', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - wrapper.setState({ - loading: false, - metrics: {}, - status: 'ERROR', - projects: [ - { conditions: [], key: 'project1', name: 'project1', status: 'ERROR' }, - { conditions: [], key: 'project2', name: 'project2', status: 'OK' }, - { conditions: [], key: 'project3', name: 'project3', status: 'WARN' } - ] - }); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.tsx new file mode 100644 index 00000000000..fc15b30e10b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGate-test.tsx @@ -0,0 +1,40 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import ApplicationQualityGate from '../ApplicationQualityGate'; + +it('renders', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ + loading: false, + metrics: {}, + status: 'ERROR', + projects: [ + { conditions: [], key: 'project1', name: 'project1', status: 'ERROR' }, + { conditions: [], key: 'project2', name: 'project2', status: 'OK' }, + { conditions: [], key: 'project3', name: 'project3', status: 'WARN' } + ] + }); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js deleted file mode 100644 index 87d7f69861d..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.js +++ /dev/null @@ -1,73 +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. - */ -// @flow -import React from 'react'; -import { shallow } from 'enzyme'; -import ApplicationQualityGateProject from '../ApplicationQualityGateProject'; - -const metrics = { - bugs: { key: 'bugs', name: 'Bugs', type: 'INT' }, - new_coverage: { key: 'new_coverage', name: 'Coverage on New Code', type: 'PERCENT' }, - skipped_tests: { key: 'skipped_tests', name: 'Skipped Tests', type: 'INT' } -}; - -it('renders', () => { - const project = { - key: 'foo', - name: 'Foo', - status: 'ERROR', - conditions: [ - { - status: 'ERROR', - metric: 'new_coverage', - comparator: 'LT', - onLeak: true, - errorThreshold: '85', - value: '82.50562381034781' - }, - { - status: 'WARN', - metric: 'bugs', - comparator: 'GT', - onLeak: false, - warningThreshold: '0', - value: '17' - }, - { - status: 'ERROR', - metric: 'bugs', - comparator: 'GT', - onLeak: true, - warningThreshold: '0', - value: '3' - }, - { - status: 'OK', - metric: 'skipped_tests', - comparator: 'GT', - onLeak: false, - warningThreshold: '0', - value: '0' - } - ] - }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.tsx new file mode 100644 index 00000000000..3f7904c67e6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/ApplicationQualityGateProject-test.tsx @@ -0,0 +1,72 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import ApplicationQualityGateProject from '../ApplicationQualityGateProject'; + +const metrics = { + bugs: { key: 'bugs', name: 'Bugs', type: 'INT' }, + new_coverage: { key: 'new_coverage', name: 'Coverage on New Code', type: 'PERCENT' }, + skipped_tests: { key: 'skipped_tests', name: 'Skipped Tests', type: 'INT' } +}; + +it('renders', () => { + const project = { + key: 'foo', + name: 'Foo', + status: 'ERROR', + conditions: [ + { + status: 'ERROR', + metric: 'new_coverage', + comparator: 'LT', + onLeak: true, + errorThreshold: '85', + value: '82.50562381034781' + }, + { + status: 'WARN', + metric: 'bugs', + comparator: 'GT', + onLeak: false, + warningThreshold: '0', + value: '17' + }, + { + status: 'ERROR', + metric: 'bugs', + comparator: 'GT', + onLeak: true, + warningThreshold: '0', + value: '3' + }, + { + status: 'OK', + metric: 'skipped_tests', + comparator: 'GT', + onLeak: false, + warningThreshold: '0', + value: '0' + } + ] + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap deleted file mode 100644 index 5a7f39cf4d9..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.js.snap +++ /dev/null @@ -1,62 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
    -

    - overview.quality_gate - -

    -
    -`; - -exports[`renders 2`] = ` -
    -

    - overview.quality_gate - -

    -
    - - -
    -
    -`; diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap new file mode 100644 index 00000000000..5a7f39cf4d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGate-test.tsx.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +
    +

    + overview.quality_gate + +

    +
    +`; + +exports[`renders 2`] = ` +
    +

    + overview.quality_gate + +

    +
    + + +
    +
    +`; diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap deleted file mode 100644 index 64d2bf86cfb..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap +++ /dev/null @@ -1,94 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` - -
    -

    - Foo -

    -
      -
    • - - - 82.5% - - - Coverage on New Code - - - quality_gates.operator.LT.short - - 85.0% - -
    • -
    • - - - 17 - - - Bugs - - - quality_gates.operator.GT.short - - 0 - -
    • -
    • - - - 3 - - - Bugs - quality_gates.conditions.leak - - - quality_gates.operator.GT.short - - 0 - -
    • -
    -
    - -`; diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap new file mode 100644 index 00000000000..64d2bf86cfb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + +
    +

    + Foo +

    +
      +
    • + + + 82.5% + + + Coverage on New Code + + + quality_gates.operator.LT.short + + 85.0% + +
    • +
    • + + + 17 + + + Bugs + + + quality_gates.operator.GT.short + + 0 + +
    • +
    • + + + 3 + + + Bugs + quality_gates.conditions.leak + + + quality_gates.operator.GT.short + + 0 + +
    • +
    +
    + +`; diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap index 2c31f8e7d5b..e286b3da99d 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap @@ -26,23 +26,9 @@ exports[`new_maintainability_rating 1`] = ` >
    @@ -82,23 +68,9 @@ exports[`new_open_issues 1`] = ` >
    @@ -150,23 +122,9 @@ exports[`new_reliability_rating 1`] = ` >
    @@ -218,23 +176,9 @@ exports[`new_security_rating 1`] = ` >
    @@ -274,17 +218,9 @@ exports[`open_issues 1`] = ` >
    @@ -335,17 +271,9 @@ exports[`reliability_rating 1`] = ` >
    @@ -396,17 +324,9 @@ exports[`security_rating 1`] = ` >
    @@ -457,17 +377,9 @@ exports[`should work with branch 1`] = ` >
    @@ -517,17 +429,9 @@ exports[`sqale_rating 1`] = ` >
    diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx index 69bd9c4b254..4029dbe7a38 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Effort.tsx @@ -42,11 +42,11 @@ export default function Effort({ component, effort, metricKey }: Props) { {' '} + className="little-spacer-right" + metricKey="projects" + metricType="SHORT_INT" + value={String(effort.projects)} + /> {translate('projects_')} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx index 5a57b31cd9d..ff7a2dd2f15 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx @@ -55,8 +55,11 @@ export default function ReleasabilityBox({ component, measures }: Props) { {' '} + className="little-spacer-right" + metricKey="projects" + metricType="SHORT_INT" + value={effort} + /> {Number(effort) === 1 ? 'project' : 'projects'} {' '} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx index 3279b530d70..75ef3f32723 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Summary.tsx @@ -41,9 +41,7 @@ export default function Summary({ component, measures }: Props) {
  • - +
    {translate('projects')}
    @@ -51,7 +49,7 @@ export default function Summary({ component, measures }: Props) {
  • - +
    {translate('metric.ncloc.name')}
    diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx index 3e297104882..2dc04f3278b 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx @@ -110,7 +110,7 @@ export default function WorstProjects({ component, subComponents, total }: Props function renderCell(measures: { [key: string]: string | undefined }, metric: string, type: string) { return ( - + ); } @@ -121,12 +121,7 @@ function renderNcloc(measures: { [key: string]: string | undefined }, maxLoc: nu return ( - + {maxLoc > 0 && ( diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap index 989531cad1e..8918c9e4b99 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap @@ -25,17 +25,11 @@ exports[`renders 1`] = ` > - projects_ , diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap index 3db58b7d539..c2120360b56 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap @@ -50,17 +50,11 @@ exports[`renders 1`] = ` > - projects diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap index f8f2706777d..149c818b286 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap @@ -32,15 +32,9 @@ exports[`renders 1`] = ` } >
  • @@ -69,15 +63,9 @@ exports[`renders 1`] = ` } >
    diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap index c36279d6709..736ab248060 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap @@ -70,60 +70,36 @@ exports[`renders 1`] = ` className="text-center" > {} ); }; diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx index 4efd5855aff..50c440bda9c 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeakMeasures.tsx @@ -37,10 +37,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
    @@ -56,10 +55,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
    @@ -75,10 +73,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
    @@ -93,10 +90,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
    {translate('metric.coverage.name')}
    @@ -107,10 +103,9 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
    @@ -122,12 +117,7 @@ export default function ProjectCardLeakMeasures({ measures }: Props) {
    - +
    {translate('metric.lines.name')}
    diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx index 63dba2d25f4..e11c8e5b80d 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx @@ -45,10 +45,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
    @@ -64,10 +63,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
    @@ -83,10 +81,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
    @@ -105,12 +102,7 @@ export default function ProjectCardOverallMeasures({ measures }: Props) { )} - +
    {translate('metric.coverage.name')}
    @@ -125,10 +117,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) { )}
    @@ -141,12 +132,7 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
    - + diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap index 0ccdace41b1..5dbdbd4a074 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeakMeasures-test.tsx.snap @@ -16,15 +16,9 @@ exports[`should render correctly with all data 1`] = ` >
    { searchProjectTags({ q: search, ps: size(this.props.facet || {}) + LIST_SIZE - }).then(result => { - if (this.mounted) { - this.setState({ isLoading: false, tags: result.tags }); - } - }); + }).then( + result => { + if (this.mounted) { + this.setState({ isLoading: false, tags: result.tags }); + } + }, + () => {} + ); } }; diff --git a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx index deb5f3d7882..18b7dbd6f95 100644 --- a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx +++ b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx @@ -20,10 +20,15 @@ import * as React from 'react'; import * as classNames from 'classnames'; +export interface BubblePopupPosition { + top: number; + right: number; +} + interface Props { customClass?: string; children: React.ReactNode; - position: { top: number; right: number }; + position: BubblePopupPosition; } export default function BubblePopup(props: Props) { diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.js b/server/sonar-web/src/main/js/components/common/MultiSelect.js deleted file mode 100644 index 2921ab1ad76..00000000000 --- a/server/sonar-web/src/main/js/components/common/MultiSelect.js +++ /dev/null @@ -1,281 +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. - */ -// @flow -import React from 'react'; -import { difference } from 'lodash'; -import MultiSelectOption from './MultiSelectOption'; -import SearchBox from '../controls/SearchBox'; -import { translate } from '../../helpers/l10n'; - -/*:: -type Props = { - selectedElements: Array, - elements: Array, - listSize: number, - onSearch: string => void, - onSelect: string => void, - onUnselect: string => void, - validateSearchInput: string => string, - placeholder: string -}; -*/ - -/*:: -type State = { - query: string, - selectedElements: Array, - unselectedElements: Array, - activeIdx: number -}; -*/ - -export default class MultiSelect extends React.PureComponent { - /*:: container: HTMLElement; */ - /*:: searchInput: HTMLInputElement; */ - /*:: props: Props; */ - /*:: state: State; */ - - static defaultProps = { - listSize: 10, - validateSearchInput: (value /*: string */) => value - }; - - constructor(props /*: Props */) { - super(props); - this.state = { - query: '', - selectedElements: [], - unselectedElements: [], - activeIdx: 0 - }; - } - - componentDidMount() { - this.updateSelectedElements(this.props); - this.updateUnselectedElements(this.props); - if (this.container) { - this.container.addEventListener('keydown', this.handleKeyboard, true); - } - } - - componentWillReceiveProps(nextProps /*: Props */) { - if ( - this.props.elements !== nextProps.elements || - this.props.selectedElements !== nextProps.selectedElements - ) { - this.updateSelectedElements(nextProps); - this.updateUnselectedElements(nextProps); - - const totalElements = this.getAllElements(nextProps, this.state).length; - if (this.state.activeIdx >= totalElements) { - this.setState({ activeIdx: totalElements - 1 }); - } - } - } - - componentDidUpdate() { - this.searchInput && this.searchInput.focus(); - } - - componentWillUnmount() { - this.container.removeEventListener('keydown', this.handleKeyboard); - } - - handleSelectChange = (item /*: string */, selected /*: boolean */) => { - if (selected) { - this.onSelectItem(item); - } else { - this.onUnselectItem(item); - } - }; - - handleSearchChange = (value /*: string */) => { - this.onSearchQuery(this.props.validateSearchInput(value)); - }; - - handleElementHover = (element /*: string */) => { - this.setState((prevState, props) => { - return { activeIdx: this.getAllElements(props, prevState).indexOf(element) }; - }); - }; - - handleKeyboard = (evt /*: KeyboardEvent */) => { - switch (evt.keyCode) { - case 40: // down - this.setState(this.selectNextElement); - evt.stopPropagation(); - evt.preventDefault(); - break; - case 38: // up - this.setState(this.selectPreviousElement); - evt.stopPropagation(); - evt.preventDefault(); - break; - case 37: // left - case 39: // right - evt.stopPropagation(); - break; - case 13: // enter - if (this.state.activeIdx >= 0) { - this.toggleSelect(this.getAllElements(this.props, this.state)[this.state.activeIdx]); - } - break; - } - }; - - onSearchQuery(query /*: string */) { - this.setState({ query, activeIdx: 0 }); - this.props.onSearch(query); - } - - onSelectItem(item /*: string */) { - if (this.isNewElement(item, this.props)) { - this.onSearchQuery(''); - } - this.props.onSelect(item); - } - - onUnselectItem(item /*: string */) { - this.props.onUnselect(item); - } - - isNewElement(elem /*: string */, { selectedElements, elements } /*: Props */) { - return elem && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1; - } - - updateSelectedElements(props /*: Props */) { - this.setState((state /*: State */) => { - if (state.query) { - return { - selectedElements: [...props.selectedElements.filter(elem => elem.includes(state.query))] - }; - } else { - return { selectedElements: [...props.selectedElements] }; - } - }); - } - - updateUnselectedElements(props /*: Props */) { - this.setState((state /*: State */) => { - if (props.listSize < state.selectedElements.length) { - return { unselectedElements: [] }; - } else { - return { - unselectedElements: difference(props.elements, props.selectedElements).slice( - 0, - props.listSize - state.selectedElements.length - ) - }; - } - }); - } - - getAllElements(props /*: Props */, state /*: State */) { - if (this.isNewElement(state.query, props)) { - return [...state.selectedElements, ...state.unselectedElements, state.query]; - } else { - return [...state.selectedElements, ...state.unselectedElements]; - } - } - - setElementActive(idx /*: number */) { - this.setState({ activeIdx: idx }); - } - - selectNextElement = (state /*: State */, props /*: Props */) => { - const { activeIdx } = state; - const allElements = this.getAllElements(props, state); - if (activeIdx < 0 || activeIdx >= allElements.length - 1) { - return { activeIdx: 0 }; - } else { - return { activeIdx: activeIdx + 1 }; - } - }; - - selectPreviousElement = (state /*: State */, props /*: Props */) => { - const { activeIdx } = state; - const allElements = this.getAllElements(props, state); - if (activeIdx <= 0) { - const lastIdx = allElements.length - 1; - return { activeIdx: lastIdx }; - } else { - return { activeIdx: activeIdx - 1 }; - } - }; - - toggleSelect(item /*: string */) { - if (this.props.selectedElements.indexOf(item) === -1) { - this.onSelectItem(item); - } else { - this.onUnselectItem(item); - } - } - - render() { - const { query, activeIdx, selectedElements, unselectedElements } = this.state; - const activeElement = this.getAllElements(this.props, this.state)[activeIdx]; - - return ( -
    (this.container = div)}> -
    - -
    -
      - {selectedElements.length > 0 && - selectedElements.map(element => ( - - ))} - {unselectedElements.length > 0 && - unselectedElements.map(element => ( - - ))} - {this.isNewElement(query, this.props) && ( - - )} -
    -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx new file mode 100644 index 00000000000..b88ccd430cf --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx @@ -0,0 +1,279 @@ +/* + * 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 * as React from 'react'; +import { difference } from 'lodash'; +import MultiSelectOption from './MultiSelectOption'; +import SearchBox from '../controls/SearchBox'; + +interface Props { + selectedElements: Array; + elements: Array; + listSize?: number; + onSearch: (query: string) => void; + onSelect: (item: string) => void; + onUnselect: (item: string) => void; + validateSearchInput?: (value: string) => string; + placeholder: string; +} + +interface State { + query: string; + selectedElements: Array; + unselectedElements: Array; + activeIdx: number; +} + +interface DefaultProps { + listSize: number; + validateSearchInput: (value: string) => string; +} + +type PropsWithDefault = Props & DefaultProps; + +export default class MultiSelect extends React.PureComponent { + container: HTMLDivElement | null; + searchInput: HTMLInputElement | null; + + static defaultProps: DefaultProps = { + listSize: 10, + validateSearchInput: (value: string) => value + }; + + constructor(props: Props) { + super(props); + this.state = { + query: '', + selectedElements: [], + unselectedElements: [], + activeIdx: 0 + }; + } + + componentDidMount() { + this.updateSelectedElements(this.props); + this.updateUnselectedElements(this.props as PropsWithDefault); + if (this.container) { + this.container.addEventListener('keydown', this.handleKeyboard, true); + } + } + + componentWillReceiveProps(nextProps: PropsWithDefault) { + if ( + this.props.elements !== nextProps.elements || + this.props.selectedElements !== nextProps.selectedElements + ) { + this.updateSelectedElements(nextProps); + this.updateUnselectedElements(nextProps); + + const totalElements = this.getAllElements(nextProps, this.state).length; + if (this.state.activeIdx >= totalElements) { + this.setState({ activeIdx: totalElements - 1 }); + } + } + } + + componentDidUpdate() { + if (this.searchInput) { + this.searchInput.focus(); + } + } + + componentWillUnmount() { + if (this.container) { + this.container.removeEventListener('keydown', this.handleKeyboard); + } + } + + handleSelectChange = (item: string, selected: boolean) => { + if (selected) { + this.onSelectItem(item); + } else { + this.onUnselectItem(item); + } + }; + + handleSearchChange = (value: string) => { + this.onSearchQuery((this.props as PropsWithDefault).validateSearchInput(value)); + }; + + handleElementHover = (element: string) => { + this.setState((prevState, props) => { + return { activeIdx: this.getAllElements(props, prevState).indexOf(element) }; + }); + }; + + handleKeyboard = (evt: KeyboardEvent) => { + switch (evt.keyCode) { + case 40: // down + this.setState(this.selectNextElement); + evt.stopPropagation(); + evt.preventDefault(); + break; + case 38: // up + this.setState(this.selectPreviousElement); + evt.stopPropagation(); + evt.preventDefault(); + break; + case 37: // left + case 39: // right + evt.stopPropagation(); + break; + case 13: // enter + if (this.state.activeIdx >= 0) { + this.toggleSelect(this.getAllElements(this.props, this.state)[this.state.activeIdx]); + } + break; + } + }; + + onSearchQuery = (query: string) => { + this.setState({ query, activeIdx: 0 }); + this.props.onSearch(query); + }; + + onSelectItem = (item: string) => { + if (this.isNewElement(item, this.props)) { + this.onSearchQuery(''); + } + this.props.onSelect(item); + }; + + onUnselectItem = (item: string) => this.props.onUnselect(item); + + isNewElement = (elem: string, { selectedElements, elements }: Props) => + elem && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1; + + updateSelectedElements = (props: Props) => { + this.setState((state: State) => { + if (state.query) { + return { + selectedElements: [...props.selectedElements.filter(elem => elem.includes(state.query))] + }; + } else { + return { selectedElements: [...props.selectedElements] }; + } + }); + }; + + updateUnselectedElements = (props: PropsWithDefault) => { + this.setState((state: State) => { + if (props.listSize < state.selectedElements.length) { + return { unselectedElements: [] }; + } else { + return { + unselectedElements: difference(props.elements, props.selectedElements).slice( + 0, + props.listSize - state.selectedElements.length + ) + }; + } + }); + }; + + getAllElements = (props: Props, state: State) => { + if (this.isNewElement(state.query, props)) { + return [...state.selectedElements, ...state.unselectedElements, state.query]; + } else { + return [...state.selectedElements, ...state.unselectedElements]; + } + }; + + setElementActive = (idx: number) => this.setState({ activeIdx: idx }); + + selectNextElement = (state: State, props: Props) => { + const { activeIdx } = state; + const allElements = this.getAllElements(props, state); + if (activeIdx < 0 || activeIdx >= allElements.length - 1) { + return { activeIdx: 0 }; + } else { + return { activeIdx: activeIdx + 1 }; + } + }; + + selectPreviousElement = (state: State, props: Props) => { + const { activeIdx } = state; + const allElements = this.getAllElements(props, state); + if (activeIdx <= 0) { + const lastIdx = allElements.length - 1; + return { activeIdx: lastIdx }; + } else { + return { activeIdx: activeIdx - 1 }; + } + }; + + toggleSelect = (item: string) => { + if (this.props.selectedElements.indexOf(item) === -1) { + this.onSelectItem(item); + } else { + this.onUnselectItem(item); + } + }; + + render() { + const { query, activeIdx, selectedElements, unselectedElements } = this.state; + const activeElement = this.getAllElements(this.props, this.state)[activeIdx]; + + return ( +
    (this.container = div)}> +
    + +
    +
      + {selectedElements.length > 0 && + selectedElements.map(element => ( + + ))} + {unselectedElements.length > 0 && + unselectedElements.map(element => ( + + ))} + {this.isNewElement(query, this.props) && ( + + )} +
    +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/components/common/MultiSelectOption.js b/server/sonar-web/src/main/js/components/common/MultiSelectOption.js deleted file mode 100644 index ad144f5cd9d..00000000000 --- a/server/sonar-web/src/main/js/components/common/MultiSelectOption.js +++ /dev/null @@ -1,75 +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. - */ -// @flow -import React from 'react'; -import classNames from 'classnames'; - -/*:: -type Props = { - element: string, - selected: boolean, - custom: boolean, - active: boolean, - onSelectChange: (string, boolean) => void, - onHover: string => void -}; -*/ - -export default class MultiSelectOption extends React.PureComponent { - /*:: props: Props; */ - - static defaultProps = { - selected: false, - custom: false, - active: false - }; - - handleSelect = (evt /*: SyntheticInputEvent */) => { - evt.stopPropagation(); - evt.preventDefault(); - evt.target.blur(); - this.props.onSelectChange(this.props.element, !this.props.selected); - }; - - handleHover = () => { - this.props.onHover(this.props.element); - }; - - render() { - const className = classNames('icon-checkbox', { - 'icon-checkbox-checked': this.props.selected - }); - const activeClass = classNames({ active: this.props.active }); - - return ( -
  • - - {this.props.custom && '+ '} - {this.props.element} - -
  • - ); - } -} diff --git a/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx b/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx new file mode 100644 index 00000000000..89429426d7e --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx @@ -0,0 +1,62 @@ +/* + * 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 * as React from 'react'; +import * as classNames from 'classnames'; + +interface Props { + element: string; + selected?: boolean; + custom?: boolean; + active?: boolean; + onSelectChange: (elem: string, selected: boolean) => void; + onHover: (elem: string) => void; +} + +export default class MultiSelectOption extends React.PureComponent { + handleSelect = (evt: React.SyntheticEvent) => { + evt.stopPropagation(); + evt.preventDefault(); + evt.currentTarget.blur(); + this.props.onSelectChange(this.props.element, !this.props.selected); + }; + + handleHover = () => this.props.onHover(this.props.element); + + render() { + const className = classNames('icon-checkbox', { + 'icon-checkbox-checked': this.props.selected + }); + const activeClass = classNames({ active: this.props.active }); + + return ( +
  • + + {this.props.custom && '+ '} + {this.props.element} + +
  • + ); + } +} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js deleted file mode 100644 index c843a7952bb..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js +++ /dev/null @@ -1,51 +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 { shallow, mount } from 'enzyme'; -import React from 'react'; -import MultiSelect from '../MultiSelect'; - -const props = { - selectedElements: ['bar'], - elements: [], - onSearch: () => {}, - onSelect: () => {}, - onUnselect: () => {}, - placeholder: '' -}; - -const elements = ['foo', 'bar', 'baz']; - -it('should render multiselect with selected elements', () => { - const multiselect = shallow(); - // Will not only the selected element - expect(multiselect).toMatchSnapshot(); - - multiselect.setProps({ elements }); - expect(multiselect).toMatchSnapshot(); - multiselect.setState({ activeIdx: 2 }); - expect(multiselect).toMatchSnapshot(); - multiselect.setState({ query: 'test' }); - expect(multiselect).toMatchSnapshot(); -}); - -it('should render with the focus inside the search input', () => { - const multiselect = mount(); - expect(multiselect.find('input').getDOMNode()).toBe(document.activeElement); -}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx new file mode 100644 index 00000000000..13bb1651866 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx @@ -0,0 +1,51 @@ +/* + * 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 * as React from 'react'; +import { shallow, mount } from 'enzyme'; +import MultiSelect from '../MultiSelect'; + +const props = { + selectedElements: ['bar'], + elements: [], + onSearch: () => {}, + onSelect: () => {}, + onUnselect: () => {}, + placeholder: '' +}; + +const elements = ['foo', 'bar', 'baz']; + +it('should render multiselect with selected elements', () => { + const multiselect = shallow(); + // Will not only the selected element + expect(multiselect).toMatchSnapshot(); + + multiselect.setProps({ elements }); + expect(multiselect).toMatchSnapshot(); + multiselect.setState({ activeIdx: 2 }); + expect(multiselect).toMatchSnapshot(); + multiselect.setState({ query: 'test' }); + expect(multiselect).toMatchSnapshot(); +}); + +it('should render with the focus inside the search input', () => { + const multiselect = mount(); + expect(multiselect.find('input').getDOMNode()).toBe(document.activeElement); +}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.js b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.js deleted file mode 100644 index a15475bc149..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.js +++ /dev/null @@ -1,47 +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 { shallow } from 'enzyme'; -import React from 'react'; -import MultiSelectOption from '../MultiSelectOption'; - -const props = { - element: 'mytag', - selected: false, - custom: false, - active: false, - onSelectChange: () => {}, - onHover: () => {} -}; - -it('should render standard tag', () => { - expect(shallow()).toMatchSnapshot(); -}); - -it('should render selected tag', () => { - expect(shallow()).toMatchSnapshot(); -}); - -it('should render custom tag', () => { - expect(shallow()).toMatchSnapshot(); -}); - -it('should render active tag', () => { - expect(shallow()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx new file mode 100644 index 00000000000..5b72d18b21e --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx @@ -0,0 +1,44 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import MultiSelectOption from '../MultiSelectOption'; + +const props = { + element: 'mytag', + onSelectChange: () => {}, + onHover: () => {} +}; + +it('should render standard tag', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render selected tag', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render custom tag', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render active tag', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap deleted file mode 100644 index 5af710f8bb1..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap +++ /dev/null @@ -1,184 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render multiselect with selected elements 1`] = ` -
    -
    - -
    -
      - -
    -
    -`; - -exports[`should render multiselect with selected elements 2`] = ` -
    -
    - -
    -
      - - - -
    -
    -`; - -exports[`should render multiselect with selected elements 3`] = ` -
    -
    - -
    -
      - - - -
    -
    -`; - -exports[`should render multiselect with selected elements 4`] = ` -
    -
    - -
    -
      - - - - -
    -
    -`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap new file mode 100644 index 00000000000..32dac980bbe --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render multiselect with selected elements 1`] = ` +
    +
    + +
    +
      + +
    +
    +`; + +exports[`should render multiselect with selected elements 2`] = ` +
    +
    + +
    +
      + + + +
    +
    +`; + +exports[`should render multiselect with selected elements 3`] = ` +
    +
    + +
    +
      + + + +
    +
    +`; + +exports[`should render multiselect with selected elements 4`] = ` +
    +
    + +
    +
      + + + + +
    +
    +`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.js.snap deleted file mode 100644 index b7b9f2a61c0..00000000000 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.js.snap +++ /dev/null @@ -1,74 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render active tag 1`] = ` -
  • - - - - mytag - -
  • -`; - -exports[`should render custom tag 1`] = ` -
  • - - - - + - mytag - -
  • -`; - -exports[`should render selected tag 1`] = ` -
  • - - - - mytag - -
  • -`; - -exports[`should render standard tag 1`] = ` -
  • - - - - mytag - -
  • -`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap new file mode 100644 index 00000000000..b7b9f2a61c0 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render active tag 1`] = ` +
  • + + + + mytag + +
  • +`; + +exports[`should render custom tag 1`] = ` +
  • + + + + + + mytag + +
  • +`; + +exports[`should render selected tag 1`] = ` +
  • + + + + mytag + +
  • +`; + +exports[`should render standard tag 1`] = ` +
  • + + + + mytag + +
  • +`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js index 30c93818fff..c334412d5c0 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js @@ -77,7 +77,6 @@ export default class SetIssueTagsPopup extends React.PureComponent { render() { return ( - // $FlowFixMe `this.props.popupPosition` is passed from `BabelPopupHelper` {'–'}
    ; - } - - const { metric } = measure; - const value = isDiffMetric(metric.key) ? measure.leak : measure.value; - +export default function Measure({ className, decimals, metricKey, metricType, value }: Props) { if (value === undefined) { return {'–'}; } - if (metric.type === 'LEVEL') { + if (metricType === 'LEVEL') { return ; } - if (metric.type !== 'RATING') { - const formattedValue = isDiffMetric(metric.key) - ? formatLeak(measure.leak, metric.key, metric.type, { decimals }) - : formatMeasure(measure.value, metric.type, { decimals }); + if (metricType !== 'RATING') { + const formattedValue = formatMeasure(value, metricType, { decimals }); return {formattedValue != null ? formattedValue : '–'}; } - const tooltip = getRatingTooltip(metric.key, Number(value)); + const tooltip = getRatingTooltip(metricKey, Number(value)); const rating = ; if (tooltip) { return ( diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx b/server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx index a62f16c513d..84aaf15fdb3 100644 --- a/server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx +++ b/server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx @@ -17,55 +17,46 @@ * 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 import/first */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Measure from '../Measure'; + jest.mock('../../../helpers/measures', () => { const measures = require.requireActual('../../../helpers/measures'); measures.getRatingTooltip = jest.fn(() => 'tooltip'); return measures; }); -import * as React from 'react'; -import { shallow } from 'enzyme'; -import Measure from '../Measure'; - it('renders trivial measure', () => { - const measure = { metric: { key: 'coverage', name: 'Coverage', type: 'PERCENT' }, value: '73.0' }; - expect(shallow()).toMatchSnapshot(); + expect( + shallow() + ).toMatchSnapshot(); }); it('renders leak measure', () => { - const measure = { - metric: { key: 'new_coverage', name: 'Coverage on New Code', type: 'PERCENT' }, - leak: '36.0' - }; - expect(shallow()).toMatchSnapshot(); + expect( + shallow() + ).toMatchSnapshot(); }); it('renders LEVEL', () => { - const measure = { - metric: { key: 'quality_gate_status', name: 'Quality Gate', type: 'LEVEL' }, - value: 'ERROR' - }; - expect(shallow()).toMatchSnapshot(); + expect( + shallow() + ).toMatchSnapshot(); }); it('renders known RATING', () => { - const measure = { - metric: { key: 'sqale_rating', name: 'Maintainability Rating', type: 'RATING' }, - value: '3' - }; - expect(shallow()).toMatchSnapshot(); + expect( + shallow() + ).toMatchSnapshot(); }); it('renders unknown RATING', () => { - const measure = { - metric: { key: 'foo_rating', name: 'Foo Rating', type: 'RATING' }, - value: '4' - }; - expect(shallow()).toMatchSnapshot(); + expect( + shallow() + ).toMatchSnapshot(); }); it('renders undefined measure', () => { - const measure = { metric: { key: 'foo', name: 'Foo', type: 'PERCENT' } }; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/measure/utils.ts b/server/sonar-web/src/main/js/components/measure/utils.ts index 8e932892152..626c65f552f 100644 --- a/server/sonar-web/src/main/js/components/measure/utils.ts +++ b/server/sonar-web/src/main/js/components/measure/utils.ts @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { - formatMeasure, - formatMeasureVariation, getRatingTooltip as nextGetRatingTooltip, isDiffMetric, Measure, @@ -40,19 +38,6 @@ export function enhanceMeasure( }; } -export function formatLeak( - value: string | undefined, - metricKey: string, - metricType: string, - options: any -): string { - if (isDiffMetric(metricKey)) { - return formatMeasure(value, metricType, options); - } else { - return formatMeasureVariation(value, metricType, options); - } -} - export function getLeakValue(measure: Measure | undefined): string | undefined { if (!measure || !measure.periods) { return undefined; diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts new file mode 100644 index 00000000000..744eed0493d --- /dev/null +++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts @@ -0,0 +1,32 @@ +/* + * 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 * as React from 'react'; +import { History } from '../../api/time-machine'; +import { Metric } from '../../app/types'; + +interface Props { + branch?: string; + history?: History; + metrics: Metric[]; + project: string; + renderWhenEmpty?: () => void; +} + +export default class PreviewGraph extends React.Component {} diff --git a/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx b/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx new file mode 100644 index 00000000000..1065e91e9ef --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx @@ -0,0 +1,150 @@ +/* + * 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 * as React from 'react'; +import { Link } from 'react-router'; +import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls'; + +const ISSUE_MEASURES = [ + 'violations', + 'new_violations', + 'blocker_violations', + 'critical_violations', + 'major_violations', + 'minor_violations', + 'info_violations', + 'new_blocker_violations', + 'new_critical_violations', + 'new_major_violations', + 'new_minor_violations', + 'new_info_violations', + 'open_issues', + 'reopened_issues', + 'confirmed_issues', + 'false_positive_issues', + 'code_smells', + 'new_code_smells', + 'bugs', + 'new_bugs', + 'vulnerabilities', + 'new_vulnerabilities' +]; + +interface Props { + branch?: string; + children?: React.ReactNode; + className?: string; + component: string; + metric: string; + sinceLeakPeriod?: boolean; +} + +export default class DrilldownLink extends React.PureComponent { + isIssueMeasure = () => { + return ISSUE_MEASURES.indexOf(this.props.metric) !== -1; + }; + + propsToIssueParams = () => { + const params: { [key: string]: string | boolean } = {}; + + if (this.props.sinceLeakPeriod) { + params.sinceLeakPeriod = true; + } + + switch (this.props.metric) { + case 'blocker_violations': + case 'new_blocker_violations': + Object.assign(params, { resolved: 'false', severities: 'BLOCKER' }); + break; + case 'critical_violations': + case 'new_critical_violations': + Object.assign(params, { resolved: 'false', severities: 'CRITICAL' }); + break; + case 'major_violations': + case 'new_major_violations': + Object.assign(params, { resolved: 'false', severities: 'MAJOR' }); + break; + case 'minor_violations': + case 'new_minor_violations': + Object.assign(params, { resolved: 'false', severities: 'MINOR' }); + break; + case 'info_violations': + case 'new_info_violations': + Object.assign(params, { resolved: 'false', severities: 'INFO' }); + break; + case 'open_issues': + Object.assign(params, { resolved: 'false', statuses: 'OPEN' }); + break; + case 'reopened_issues': + Object.assign(params, { resolved: 'false', statuses: 'REOPENED' }); + break; + case 'confirmed_issues': + Object.assign(params, { resolved: 'false', statuses: 'CONFIRMED' }); + break; + case 'false_positive_issues': + Object.assign(params, { resolutions: 'FALSE-POSITIVE' }); + break; + case 'code_smells': + case 'new_code_smells': + Object.assign(params, { resolved: 'false', types: 'CODE_SMELL' }); + break; + case 'bugs': + case 'new_bugs': + Object.assign(params, { resolved: 'false', types: 'BUG' }); + break; + case 'vulnerabilities': + case 'new_vulnerabilities': + Object.assign(params, { resolved: 'false', types: 'VULNERABILITY' }); + break; + default: + Object.assign(params, { resolved: 'false' }); + } + return params; + }; + + renderIssuesLink = () => { + const url = getComponentIssuesUrl(this.props.component, { + ...this.propsToIssueParams(), + branch: this.props.branch + }); + + return ( + + {this.props.children} + + ); + }; + + render() { + if (this.isIssueMeasure()) { + return this.renderIssuesLink(); + } + + const url = getComponentDrilldownUrl( + this.props.component, + this.props.metric, + this.props.branch + ); + return ( + + {this.props.children} + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/shared/drilldown-link.js b/server/sonar-web/src/main/js/components/shared/drilldown-link.js deleted file mode 100644 index cdff767a2d9..00000000000 --- a/server/sonar-web/src/main/js/components/shared/drilldown-link.js +++ /dev/null @@ -1,150 +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 React from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router'; -import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls'; - -const ISSUE_MEASURES = [ - 'violations', - 'new_violations', - 'blocker_violations', - 'critical_violations', - 'major_violations', - 'minor_violations', - 'info_violations', - 'new_blocker_violations', - 'new_critical_violations', - 'new_major_violations', - 'new_minor_violations', - 'new_info_violations', - 'open_issues', - 'reopened_issues', - 'confirmed_issues', - 'false_positive_issues', - 'code_smells', - 'new_code_smells', - 'bugs', - 'new_bugs', - 'vulnerabilities', - 'new_vulnerabilities' -]; - -export class DrilldownLink extends React.PureComponent { - static propTypes = { - branch: PropTypes.string, - children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), - className: PropTypes.string, - component: PropTypes.string.isRequired, - metric: PropTypes.string.isRequired, - sinceLeakPeriod: PropTypes.bool - }; - isIssueMeasure = () => { - return ISSUE_MEASURES.indexOf(this.props.metric) !== -1; - }; - - propsToIssueParams = () => { - const params = {}; - - if (this.props.sinceLeakPeriod) { - params.sinceLeakPeriod = true; - } - - switch (this.props.metric) { - case 'blocker_violations': - case 'new_blocker_violations': - Object.assign(params, { resolved: 'false', severities: 'BLOCKER' }); - break; - case 'critical_violations': - case 'new_critical_violations': - Object.assign(params, { resolved: 'false', severities: 'CRITICAL' }); - break; - case 'major_violations': - case 'new_major_violations': - Object.assign(params, { resolved: 'false', severities: 'MAJOR' }); - break; - case 'minor_violations': - case 'new_minor_violations': - Object.assign(params, { resolved: 'false', severities: 'MINOR' }); - break; - case 'info_violations': - case 'new_info_violations': - Object.assign(params, { resolved: 'false', severities: 'INFO' }); - break; - case 'open_issues': - Object.assign(params, { resolved: 'false', statuses: 'OPEN' }); - break; - case 'reopened_issues': - Object.assign(params, { resolved: 'false', statuses: 'REOPENED' }); - break; - case 'confirmed_issues': - Object.assign(params, { resolved: 'false', statuses: 'CONFIRMED' }); - break; - case 'false_positive_issues': - Object.assign(params, { resolutions: 'FALSE-POSITIVE' }); - break; - case 'code_smells': - case 'new_code_smells': - Object.assign(params, { resolved: 'false', types: 'CODE_SMELL' }); - break; - case 'bugs': - case 'new_bugs': - Object.assign(params, { resolved: 'false', types: 'BUG' }); - break; - case 'vulnerabilities': - case 'new_vulnerabilities': - Object.assign(params, { resolved: 'false', types: 'VULNERABILITY' }); - break; - default: - Object.assign(params, { resolved: 'false' }); - } - return params; - }; - - renderIssuesLink = () => { - const url = getComponentIssuesUrl(this.props.component, { - ...this.propsToIssueParams(), - branch: this.props.branch - }); - - return ( - - {this.props.children} - - ); - }; - - render() { - if (this.isIssueMeasure()) { - return this.renderIssuesLink(); - } - - const url = getComponentDrilldownUrl( - this.props.component, - this.props.metric, - this.props.branch - ); - return ( - - {this.props.children} - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/tags/TagsSelector.js b/server/sonar-web/src/main/js/components/tags/TagsSelector.js deleted file mode 100644 index c257c31a5ed..00000000000 --- a/server/sonar-web/src/main/js/components/tags/TagsSelector.js +++ /dev/null @@ -1,61 +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. - */ -// @flow -import React from 'react'; -import BubblePopup from '../common/BubblePopup'; -import MultiSelect from '../common/MultiSelect'; -import { translate } from '../../helpers/l10n'; -import './TagsList.css'; - -/*:: -type Props = { - position: {}, - tags: Array, - selectedTags: Array, - listSize: number, - onSearch: string => void, - onSelect: string => void, - onUnselect: string => void -}; -*/ - -export default function TagsSelector(props /*: Props */) { - return ( - - - - ); -} - -export function validateTag(value /*: string */) { - // Allow only a-z, 0-9, '+', '-', '#', '.' - return value.toLowerCase().replace(/[^a-z0-9\+\-#.]/gi, ''); -} diff --git a/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx b/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx new file mode 100644 index 00000000000..f9ed202989c --- /dev/null +++ b/server/sonar-web/src/main/js/components/tags/TagsSelector.tsx @@ -0,0 +1,58 @@ +/* + * 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 * as React from 'react'; +import BubblePopup, { BubblePopupPosition } from '../common/BubblePopup'; +import MultiSelect from '../common/MultiSelect'; +import { translate } from '../../helpers/l10n'; +import './TagsList.css'; + +interface Props { + position: BubblePopupPosition; + tags: string[]; + selectedTags: string[]; + listSize: number; + onSearch: (query: string) => void; + onSelect: (item: string) => void; + onUnselect: (item: string) => void; +} + +export default function TagsSelector(props: Props) { + return ( + + + + ); +} + +export function validateTag(value: string) { + // Allow only a-z, 0-9, '+', '-', '#', '.' + return value.toLowerCase().replace(/[^a-z0-9\+\-#.]/gi, ''); +} diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js b/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js deleted file mode 100644 index 98186daca4a..00000000000 --- a/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js +++ /dev/null @@ -1,49 +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 { shallow } from 'enzyme'; -import React from 'react'; -import TagsSelector, { validateTag } from '../TagsSelector'; - -const props = { - position: { left: 0, top: 0 }, - tags: ['foo', 'bar', 'baz'], - selectedTags: ['bar'], - onSearch: () => {}, - onSelect: () => {}, - onUnselect: () => {} -}; - -it('should render with selected tags', () => { - const tagsSelector = shallow(); - expect(tagsSelector).toMatchSnapshot(); -}); - -it('should render without tags at all', () => { - expect(shallow()).toMatchSnapshot(); -}); - -it('should validate tags correctly', () => { - const validChars = 'abcdefghijklmnopqrstuvwxyz0123456789+-#.'; - expect(validateTag('test')).toBe('test'); - expect(validateTag(validChars)).toBe(validChars); - expect(validateTag(validChars.toUpperCase())).toBe(validChars); - expect(validateTag('T E$ST')).toBe('test'); - expect(validateTag('T E$st!^àéèing1')).toBe('testing1'); -}); diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx b/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx new file mode 100644 index 00000000000..1b5169a2940 --- /dev/null +++ b/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx @@ -0,0 +1,50 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import TagsSelector, { validateTag } from '../TagsSelector'; + +const props = { + position: { right: 0, top: 0 }, + listSize: 10, + tags: ['foo', 'bar', 'baz'], + selectedTags: ['bar'], + onSearch: () => {}, + onSelect: () => {}, + onUnselect: () => {} +}; + +it('should render with selected tags', () => { + const tagsSelector = shallow(); + expect(tagsSelector).toMatchSnapshot(); +}); + +it('should render without tags at all', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should validate tags correctly', () => { + const validChars = 'abcdefghijklmnopqrstuvwxyz0123456789+-#.'; + expect(validateTag('test')).toBe('test'); + expect(validateTag(validChars)).toBe(validChars); + expect(validateTag(validChars.toUpperCase())).toBe(validChars); + expect(validateTag('T E$ST')).toBe('test'); + expect(validateTag('T E$st!^àéèing1')).toBe('testing1'); +}); diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap deleted file mode 100644 index e2d57569b31..00000000000 --- a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render with selected tags 1`] = ` - - - -`; - -exports[`should render without tags at all 1`] = ` - - - -`; diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap new file mode 100644 index 00000000000..c2d4623b8c8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render with selected tags 1`] = ` + + + +`; + +exports[`should render without tags at all 1`] = ` + + + +`; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts index 68ae35138a7..6cf02992461 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { resetBundle } from '../l10n'; -import { formatMeasure, formatMeasureVariation } from '../measures'; +import { formatMeasure } from '../measures'; const HOURS_IN_DAY = 8; const ONE_MINUTE = 1; @@ -170,92 +170,3 @@ describe('#formatMeasure()', () => { expect(formatMeasure(undefined, 'INT')).toBe(''); }); }); - -describe('#formatMeasureVariation()', () => { - it('should format INT', () => { - expect(formatMeasureVariation(0, 'INT')).toBe('+0'); - expect(formatMeasureVariation(1, 'INT')).toBe('+1'); - expect(formatMeasureVariation(-1, 'INT')).toBe('-1'); - expect(formatMeasureVariation(1529, 'INT')).toBe('+1,529'); - expect(formatMeasureVariation(-1529, 'INT')).toBe('-1,529'); - }); - - it('should format SHORT_INT', () => { - expect(formatMeasureVariation(0, 'SHORT_INT')).toBe('+0'); - expect(formatMeasureVariation(1, 'SHORT_INT')).toBe('+1'); - expect(formatMeasureVariation(-1, 'SHORT_INT')).toBe('-1'); - expect(formatMeasureVariation(1529, 'SHORT_INT')).toBe('+1.5k'); - expect(formatMeasureVariation(-1529, 'SHORT_INT')).toBe('-1.5k'); - expect(formatMeasureVariation(10678, 'SHORT_INT')).toBe('+11k'); - expect(formatMeasureVariation(-10678, 'SHORT_INT')).toBe('-11k'); - }); - - it('should format FLOAT', () => { - expect(formatMeasureVariation(0.0, 'FLOAT')).toBe('+0.0'); - expect(formatMeasureVariation(1.0, 'FLOAT')).toBe('+1.0'); - expect(formatMeasureVariation(-1.0, 'FLOAT')).toBe('-1.0'); - expect(formatMeasureVariation(50.89, 'FLOAT')).toBe('+50.89'); - expect(formatMeasureVariation(-50.89, 'FLOAT')).toBe('-50.89'); - }); - - it('should respect FLOAT precision', () => { - expect(formatMeasureVariation(0.1, 'FLOAT')).toBe('+0.1'); - expect(formatMeasureVariation(0.12, 'FLOAT')).toBe('+0.12'); - expect(formatMeasureVariation(0.12345, 'FLOAT')).toBe('+0.12345'); - expect(formatMeasureVariation(0.123456, 'FLOAT')).toBe('+0.12346'); - }); - - it('should format PERCENT', () => { - expect(formatMeasureVariation(0.0, 'PERCENT')).toBe('+0.0%'); - expect(formatMeasureVariation(1.0, 'PERCENT')).toBe('+1.0%'); - expect(formatMeasureVariation(-1.0, 'PERCENT')).toBe('-1.0%'); - expect(formatMeasureVariation(50.89, 'PERCENT')).toBe('+50.9%'); - expect(formatMeasureVariation(-50.89, 'PERCENT')).toBe('-50.9%'); - }); - - it('should format WORK_DUR', () => { - expect(formatMeasureVariation(0, 'WORK_DUR')).toBe('+0'); - expect(formatMeasureVariation(5 * ONE_DAY, 'WORK_DUR')).toBe('+5d'); - expect(formatMeasureVariation(2 * ONE_HOUR, 'WORK_DUR')).toBe('+2h'); - expect(formatMeasureVariation(ONE_MINUTE, 'WORK_DUR')).toBe('+1min'); - expect(formatMeasureVariation(-5 * ONE_DAY, 'WORK_DUR')).toBe('-5d'); - expect(formatMeasureVariation(-2 * ONE_HOUR, 'WORK_DUR')).toBe('-2h'); - expect(formatMeasureVariation(-1 * ONE_MINUTE, 'WORK_DUR')).toBe('-1min'); - }); - - it('should format SHORT_WORK_DUR', () => { - expect(formatMeasureVariation(0, 'SHORT_WORK_DUR')).toBe('+0'); - expect(formatMeasureVariation(5 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('+5d'); - expect(formatMeasureVariation(2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('+2h'); - expect(formatMeasureVariation(ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+1min'); - expect(formatMeasureVariation(30 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+30min'); - expect(formatMeasureVariation(58 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+1h'); - expect(formatMeasureVariation(5 * ONE_DAY + 2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('+5d'); - expect(formatMeasureVariation(2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+2h'); - expect(formatMeasureVariation(ONE_HOUR + 55 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+2h'); - expect(formatMeasureVariation(3 * ONE_DAY + 6 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('+4d'); - expect(formatMeasureVariation(7 * ONE_HOUR + 59 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+1d'); - expect(formatMeasureVariation(5 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe( - '+5d' - ); - expect(formatMeasureVariation(15 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe( - '+15d' - ); - expect(formatMeasureVariation(7 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('+7min'); - expect(formatMeasureVariation(-5 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('-5d'); - expect(formatMeasureVariation(-2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('-2h'); - expect(formatMeasureVariation(-1 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('-1min'); - - expect(formatMeasureVariation(1529 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('+1.5kd'); - expect(formatMeasureVariation(1234567 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('+1Md'); - expect(formatMeasureVariation(1234567 * ONE_DAY + 2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('+1Md'); - }); - - it('should not format unknown type', () => { - expect(formatMeasureVariation('random value', 'RANDOM_TYPE')).toBe('random value'); - }); - - it('should not fail with undefined', () => { - expect(formatMeasureVariation(undefined, 'INT')).toBe(''); - }); -}); diff --git a/server/sonar-web/src/main/js/helpers/measures.ts b/server/sonar-web/src/main/js/helpers/measures.ts index edb05d8ce85..b6354557ad1 100644 --- a/server/sonar-web/src/main/js/helpers/measures.ts +++ b/server/sonar-web/src/main/js/helpers/measures.ts @@ -55,16 +55,6 @@ export function formatMeasure( return useFormatter(value, formatter, options); } -/** Format a measure variation for a given type */ -export function formatMeasureVariation( - value: string | number | undefined, - type: string, - options?: any -): string { - const formatter = getVariationFormatter(type); - return useFormatter(value, formatter, options); -} - /** Return a localized metric name */ export function localizeMetric(metricKey: string): string { return translate('metric', metricKey, 'name'); @@ -91,7 +81,10 @@ export function enhanceMeasuresWithMetrics( } /** Get period value of a measure */ -export function getPeriodValue(measure: Measure, periodIndex: number): string | undefined { +export function getPeriodValue( + measure: Measure | MeasureEnhanced, + periodIndex: number +): string | undefined { const { periods } = measure; const period = periods && periods.find(period => period.index === periodIndex); return period ? period.value : undefined; @@ -125,21 +118,6 @@ function getFormatter(type: string): Formatter { return FORMATTERS[type] || noFormatter; } -function getVariationFormatter(type: string): Formatter { - const FORMATTERS: { [type: string]: Formatter } = { - INT: intVariationFormatter, - SHORT_INT: shortIntVariationFormatter, - FLOAT: floatVariationFormatter, - PERCENT: percentVariationFormatter, - WORK_DUR: durationVariationFormatter, - SHORT_WORK_DUR: shortDurationVariationFormatter, - RATING: emptyFormatter, - LEVEL: emptyFormatter, - MILLISEC: millisecondsVariationFormatter - }; - return FORMATTERS[type] || noFormatter; -} - function numberFormatter( value: number, minimumFractionDigits = 0, @@ -156,19 +134,10 @@ function noFormatter(value: string | number): string | number { return value; } -function emptyFormatter(): string { - return ''; -} - function intFormatter(value: number): string { return numberFormatter(value); } -function intVariationFormatter(value: number): string { - const prefix = value < 0 ? '-' : '+'; - return prefix + intFormatter(Math.abs(value)); -} - function shortIntFormatter(value: number): string { if (value >= 1e9) { return numberFormatter(value / 1e9) + translate('short_number_suffix.g'); @@ -183,20 +152,10 @@ function shortIntFormatter(value: number): string { } } -function shortIntVariationFormatter(value: number): string { - const formatted = shortIntFormatter(Math.abs(value)); - return value < 0 ? `-${formatted}` : `+${formatted}`; -} - function floatFormatter(value: number): string { return numberFormatter(value, 1, 5); } -function floatVariationFormatter(value: number): string { - const prefix = value < 0 ? '-' : '+'; - return prefix + floatFormatter(Math.abs(value)); -} - function percentFormatter(value: string | number, options: { decimals?: number } = {}): string { if (typeof value === 'string') { value = parseFloat(value); @@ -207,17 +166,6 @@ function percentFormatter(value: string | number, options: { decimals?: number } return value === 100 ? '100%' : numberFormatter(value, 1) + '%'; } -function percentVariationFormatter( - value: string | number, - options: { decimals?: number } = {} -): string { - if (typeof value === 'string') { - value = parseFloat(value); - } - const prefix = value < 0 ? '-' : '+'; - return prefix + percentFormatter(Math.abs(value), options); -} - function ratingFormatter(value: string | number): string { if (typeof value === 'string') { value = parseInt(value, 10); @@ -247,12 +195,6 @@ function millisecondsFormatter(value: number): string { } } -function millisecondsVariationFormatter(value: number): string { - const absValue = Math.abs(value); - const formattedValue = millisecondsFormatter(absValue); - return value < 0 ? `-${formattedValue}` : `+${formattedValue}`; -} - /* * Debt Formatters */ @@ -362,22 +304,6 @@ function shortDurationFormatter(value: string | number): string { return formatDurationShort(isNegative, days, hours, remainingValue); } -function durationVariationFormatter(value: string | number): string { - if (value === 0 || value === '0') { - return '+0'; - } - const formatted = durationFormatter(value); - return formatted[0] !== '-' ? '+' + formatted : formatted; -} - -function shortDurationVariationFormatter(value: string | number): string { - if (value === 0 || value === '0') { - return '+0'; - } - const formatted = shortDurationFormatter(value); - return formatted[0] !== '-' ? '+' + formatted : formatted; -} - function getRatingGrid(): string { // workaround cyclic dependencies const getStore = require('../app/utils/getStore').default; @@ -429,12 +355,12 @@ function getMaintainabilityRatingTooltip(rating: number): string { ); } -export function getRatingTooltip(metricKey: string, value: number): string { +export function getRatingTooltip(metricKey: string, value: number | string): string { const ratingLetter = formatMeasure(value, 'RATING'); - const finalMetricKey = metricKey.startsWith('new_') ? metricKey.substr(4) : metricKey; + const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey; return finalMetricKey === 'sqale_rating' || finalMetricKey === 'maintainability_rating' - ? getMaintainabilityRatingTooltip(value) + ? getMaintainabilityRatingTooltip(Number(value)) : translate('metric', finalMetricKey, 'tooltip', ratingLetter); } diff --git a/server/sonar-web/src/main/js/helpers/periods.ts b/server/sonar-web/src/main/js/helpers/periods.ts index 110eb67cf2a..afc0edf1c49 100644 --- a/server/sonar-web/src/main/js/helpers/periods.ts +++ b/server/sonar-web/src/main/js/helpers/periods.ts @@ -54,8 +54,8 @@ export function getPeriodLabel(period: Period | undefined): string | undefined { return translateWithParameters(`overview.period.${period.mode}`, parameter); } -export function getPeriodDate(period: Period | undefined): Date | undefined { - return period ? parseDate(period.date) : undefined; +export function getPeriodDate(period?: { date?: string }): Date | undefined { + return period && period.date ? parseDate(period.date) : undefined; } export function getLeakPeriodLabel(periods: Period[]): string | undefined {