diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-11-07 11:23:43 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-11-20 20:20:59 +0100 |
commit | c9e3474940286b65bd94d14eeacef982407a65af (patch) | |
tree | 601a704d493a9f38abcc2bf4659f9bb62cacd7c3 /server/sonar-web/src | |
parent | 5a5c9bfb9fb4c3591759ef444524c7d8097198a6 (diff) | |
download | sonarqube-c9e3474940286b65bd94d14eeacef982407a65af.tar.gz sonarqube-c9e3474940286b65bd94d14eeacef982407a65af.zip |
SONAR-10770 display definition change events for applications (#918)
Diffstat (limited to 'server/sonar-web/src')
13 files changed, 584 insertions, 19 deletions
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 5314aed9833..faea1ccdef2 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { differenceBy } from 'lodash'; +import { ComponentContext } from './ComponentContext'; import ComponentContainerNotFound from './ComponentContainerNotFound'; import ComponentNav from './nav/component/ComponentNav'; import { Component, BranchLike, Measure, Task } from '../types'; @@ -365,15 +366,17 @@ export class ComponentContainer extends React.PureComponent<Props, State> { <i className="spinner" /> </div> ) : ( - React.cloneElement(this.props.children, { - branchLike, - branchLikes, - component, - isInProgress, - isPending, - onBranchesChange: this.handleBranchesChange, - onComponentChange: this.handleComponentChange - }) + <ComponentContext.Provider value={{ branchLike, component }}> + {React.cloneElement(this.props.children, { + branchLike, + branchLikes, + component, + isInProgress, + isPending, + onBranchesChange: this.handleBranchesChange, + onComponentChange: this.handleComponentChange + })} + </ComponentContext.Provider> )} </div> ); diff --git a/server/sonar-web/src/main/js/app/components/ComponentContext.tsx b/server/sonar-web/src/main/js/app/components/ComponentContext.tsx new file mode 100644 index 00000000000..b62a351cd3f --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/ComponentContext.tsx @@ -0,0 +1,31 @@ +/* + * 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 { Component, BranchLike } from '../types'; + +interface ComponentContextType { + branchLike: BranchLike | undefined; + component: Component | undefined; +} + +export const ComponentContext = React.createContext<ComponentContextType>({ + branchLike: undefined, + component: undefined +}); diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 8774f66a0ec..a9fc074cc19 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -62,6 +62,14 @@ export interface AnalysisEvent { status: string; stillFailing: boolean; }; + projects?: Array<{ + branch?: string; + changeType: string; + key: string; + name: string; + newBranch?: string; + oldBranch?: string; + }>; } export interface AppState { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx new file mode 100644 index 00000000000..5a7f71acfc9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx @@ -0,0 +1,168 @@ +/* + * 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 { FormattedMessage } from 'react-intl'; +import * as classNames from 'classnames'; +import { AnalysisEvent, BranchLike } from '../../../app/types'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon'; +import { ResetButtonLink } from '../../../components/ui/buttons'; +import { translate } from '../../../helpers/l10n'; +import { getProjectUrl } from '../../../helpers/urls'; +import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon'; +import { isMainBranch } from '../../../helpers/branches'; + +export type DefinitionChangeEvent = AnalysisEvent & Required<Pick<AnalysisEvent, 'projects'>>; + +export function isDefinitionChangeEvent(event: AnalysisEvent): event is DefinitionChangeEvent { + return event.category === 'DEFINITION_CHANGE' && event.projects !== undefined; +} + +interface Props { + branchLike: BranchLike | undefined; + event: DefinitionChangeEvent; +} + +interface State { + expanded: boolean; +} + +export class DefinitionChangeEventInner extends React.PureComponent<Props, State> { + state: State = { expanded: false }; + + toggleProjectsList = () => { + this.setState(state => ({ expanded: !state.expanded })); + }; + + renderProjectLink = (project: { key: string; name: string }, branch: string | undefined) => ( + <Link onClick={e => e.stopPropagation()} to={getProjectUrl(project.key, branch)}> + {project.name} + </Link> + ); + + renderBranch = (branch: string | undefined) => ( + <span className="nowrap"> + <LongLivingBranchIcon className="little-spacer-left text-text-top" /> + {branch} + </span> + ); + + renderProjectChange(project: { + changeType: string; + key: string; + name: string; + branch?: string; + newBranch?: string; + oldBranch?: string; + }) { + const mainBranch = !this.props.branchLike || isMainBranch(this.props.branchLike); + + if (project.changeType === 'ADDED') { + const message = mainBranch + ? 'event.definition_change.added' + : 'event.definition_change.branch_added'; + return ( + <div className="text-ellipsis"> + <FormattedMessage + defaultMessage={translate(message)} + id={message} + values={{ + project: this.renderProjectLink(project, project.branch), + branch: this.renderBranch(project.branch) + }} + /> + </div> + ); + } else if (project.changeType === 'REMOVED') { + const message = mainBranch + ? 'event.definition_change.removed' + : 'event.definition_change.branch_removed'; + return ( + <div className="text-ellipsis"> + <FormattedMessage + defaultMessage={translate(message)} + id={message} + values={{ + project: this.renderProjectLink(project, project.branch), + branch: this.renderBranch(project.branch) + }} + /> + </div> + ); + } else if (project.changeType === 'BRANCH_CHANGED') { + return ( + <FormattedMessage + defaultMessage={translate('event.definition_change.branch_replaced')} + id={'event.definition_change.branch_replaced'} + values={{ + project: this.renderProjectLink(project, project.newBranch), + oldBranch: this.renderBranch(project.oldBranch), + newBranch: this.renderBranch(project.newBranch) + }} + /> + ); + } + + return null; + } + + render() { + const { event } = this.props; + const { expanded } = this.state; + return ( + <div className="project-activity-event-inner"> + <div className="project-activity-event-inner-main"> + <ProjectEventIcon + className={classNames( + 'project-activity-event-icon', + 'little-spacer-right', + event.category + )} + /> + + <div className="project-activity-event-inner-text flex-1"> + <span className="note little-spacer-right"> + {translate('event.category', event.category)} + </span> + </div> + + <ResetButtonLink + className="project-activity-event-inner-more-link" + onClick={this.toggleProjectsList} + stopPropagation={true}> + {expanded ? translate('hide') : translate('more')} + <DropdownIcon className="little-spacer-left" turned={expanded} /> + </ResetButtonLink> + </div> + + {expanded && ( + <ul> + {event.projects.map(project => ( + <li className="display-flex-center little-spacer-top" key={project.key}> + {this.renderProjectChange(project)} + </li> + ))} + </ul> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx index 4d66330e64e..69369b52d4a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx @@ -20,7 +20,9 @@ import * as React from 'react'; import * as classNames from 'classnames'; import { isRichQualityGateEvent, RichQualityGateEventInner } from './RichQualityGateEventInner'; +import { isDefinitionChangeEvent, DefinitionChangeEventInner } from './DefinitionChangeEventInner'; import { AnalysisEvent } from '../../../app/types'; +import { ComponentContext } from '../../../app/components/ComponentContext'; import ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon'; import { translate } from '../../../helpers/l10n'; @@ -31,6 +33,12 @@ interface Props { export default function EventInner({ event }: Props) { if (isRichQualityGateEvent(event)) { return <RichQualityGateEventInner event={event} />; + } else if (isDefinitionChangeEvent(event)) { + return ( + <ComponentContext.Consumer> + {({ branchLike }) => <DefinitionChangeEventInner branchLike={branchLike} event={event} />} + </ComponentContext.Consumer> + ); } else { return ( <div className="project-activity-event-inner"> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.tsx index c2519a32c71..8136cfcc71a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import ProjectActivityEventSelectOption from './ProjectActivityEventSelectOption'; import ProjectActivityEventSelectValue from './ProjectActivityEventSelectValue'; import ProjectActivityDateInput from './ProjectActivityDateInput'; @@ -39,8 +40,8 @@ export default class ProjectActivityPageHeader extends React.PureComponent<Props this.props.updateQuery({ category: option ? option.value : '' }); render() { - const eventTypes = - this.props.project.qualifier === 'APP' ? APPLICATION_EVENT_TYPES : EVENT_TYPES; + const isApp = this.props.project.qualifier === 'APP'; + const eventTypes = isApp ? APPLICATION_EVENT_TYPES : EVENT_TYPES; const options = eventTypes.map(category => ({ label: translate('event.category', category), value: category @@ -50,7 +51,10 @@ export default class ProjectActivityPageHeader extends React.PureComponent<Props <header className="page-header"> {!['VW', 'SVW'].includes(this.props.project.qualifier) && ( <Select - className="input-medium pull-left big-spacer-right" + className={classNames('pull-left big-spacer-right', { + 'input-medium': !isApp, + 'input-large': isApp + })} clearable={true} onChange={this.handleCategoryChange} optionComponent={ProjectActivityEventSelectOption} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx index 780668cf39d..74e02a77421 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx @@ -29,8 +29,7 @@ import Level from '../../../components/ui/Level'; import { translate } from '../../../helpers/l10n'; import { getProjectUrl } from '../../../helpers/urls'; -export type RichQualityGateEvent = Exclude<AnalysisEvent, 'qualityGate'> & - Required<Pick<AnalysisEvent, 'qualityGate'>>; +export type RichQualityGateEvent = AnalysisEvent & Required<Pick<AnalysisEvent, 'qualityGate'>>; export function isRichQualityGateEvent(event: AnalysisEvent): event is RichQualityGateEvent { return event.category === 'QUALITY_GATE' && event.qualityGate !== undefined; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/DefinitionChangeEventInner-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/DefinitionChangeEventInner-test.tsx new file mode 100644 index 00000000000..5e5a626df17 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/DefinitionChangeEventInner-test.tsx @@ -0,0 +1,65 @@ +/* + * 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 { DefinitionChangeEventInner, DefinitionChangeEvent } from '../DefinitionChangeEventInner'; +import { click } from '../../../../helpers/testUtils'; +import { LongLivingBranch, BranchType } from '../../../../app/types'; + +it('should render', () => { + const event: DefinitionChangeEvent = { + category: 'DEFINITION_CHANGE', + key: 'foo1234', + name: '', + projects: [ + { changeType: 'ADDED', key: 'foo', name: 'Foo', branch: 'master' }, + { changeType: 'REMOVED', key: 'bar', name: 'Bar', branch: 'master' } + ] + }; + const wrapper = shallow(<DefinitionChangeEventInner branchLike={undefined} event={event} />); + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('.project-activity-event-inner-more-link')); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render for a branch', () => { + const branch: LongLivingBranch = { name: 'feature-x', isMain: false, type: BranchType.LONG }; + const event: DefinitionChangeEvent = { + category: 'DEFINITION_CHANGE', + key: 'foo1234', + name: '', + projects: [ + { changeType: 'ADDED', key: 'foo', name: 'Foo', branch: 'feature-x' }, + { + changeType: 'BRANCH_CHANGED', + key: 'bar', + name: 'Bar', + oldBranch: 'master', + newBranch: 'feature-y' + } + ] + }; + const wrapper = shallow(<DefinitionChangeEventInner branchLike={branch} event={event} />); + click(wrapper.find('.project-activity-event-inner-more-link')); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap new file mode 100644 index 00000000000..ce671b44ab4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap @@ -0,0 +1,275 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<div + className="project-activity-event-inner" +> + <div + className="project-activity-event-inner-main" + > + <ProjectEventIcon + className="project-activity-event-icon little-spacer-right DEFINITION_CHANGE" + /> + <div + className="project-activity-event-inner-text flex-1" + > + <span + className="note little-spacer-right" + > + event.category.DEFINITION_CHANGE + </span> + </div> + <ResetButtonLink + className="project-activity-event-inner-more-link" + onClick={[Function]} + stopPropagation={true} + > + more + <DropdownIcon + className="little-spacer-left" + turned={false} + /> + </ResetButtonLink> + </div> +</div> +`; + +exports[`should render 2`] = ` +<div + className="project-activity-event-inner" +> + <div + className="project-activity-event-inner-main" + > + <ProjectEventIcon + className="project-activity-event-icon little-spacer-right DEFINITION_CHANGE" + /> + <div + className="project-activity-event-inner-text flex-1" + > + <span + className="note little-spacer-right" + > + event.category.DEFINITION_CHANGE + </span> + </div> + <ResetButtonLink + className="project-activity-event-inner-more-link" + onClick={[Function]} + stopPropagation={true} + > + hide + <DropdownIcon + className="little-spacer-left" + turned={true} + /> + </ResetButtonLink> + </div> + <ul> + <li + className="display-flex-center little-spacer-top" + key="foo" + > + <div + className="text-ellipsis" + > + <FormattedMessage + defaultMessage="event.definition_change.added" + id="event.definition_change.added" + values={ + Object { + "branch": <span + className="nowrap" + > + <LongLivingBranchIcon + className="little-spacer-left text-text-top" + /> + master + </span>, + "project": <Link + onClick={[Function]} + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": "master", + "id": "foo", + }, + } + } + > + Foo + </Link>, + } + } + /> + </div> + </li> + <li + className="display-flex-center little-spacer-top" + key="bar" + > + <div + className="text-ellipsis" + > + <FormattedMessage + defaultMessage="event.definition_change.removed" + id="event.definition_change.removed" + values={ + Object { + "branch": <span + className="nowrap" + > + <LongLivingBranchIcon + className="little-spacer-left text-text-top" + /> + master + </span>, + "project": <Link + onClick={[Function]} + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": "master", + "id": "bar", + }, + } + } + > + Bar + </Link>, + } + } + /> + </div> + </li> + </ul> +</div> +`; + +exports[`should render for a branch 1`] = ` +<div + className="project-activity-event-inner" +> + <div + className="project-activity-event-inner-main" + > + <ProjectEventIcon + className="project-activity-event-icon little-spacer-right DEFINITION_CHANGE" + /> + <div + className="project-activity-event-inner-text flex-1" + > + <span + className="note little-spacer-right" + > + event.category.DEFINITION_CHANGE + </span> + </div> + <ResetButtonLink + className="project-activity-event-inner-more-link" + onClick={[Function]} + stopPropagation={true} + > + hide + <DropdownIcon + className="little-spacer-left" + turned={true} + /> + </ResetButtonLink> + </div> + <ul> + <li + className="display-flex-center little-spacer-top" + key="foo" + > + <div + className="text-ellipsis" + > + <FormattedMessage + defaultMessage="event.definition_change.branch_added" + id="event.definition_change.branch_added" + values={ + Object { + "branch": <span + className="nowrap" + > + <LongLivingBranchIcon + className="little-spacer-left text-text-top" + /> + feature-x + </span>, + "project": <Link + onClick={[Function]} + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": "feature-x", + "id": "foo", + }, + } + } + > + Foo + </Link>, + } + } + /> + </div> + </li> + <li + className="display-flex-center little-spacer-top" + key="bar" + > + <FormattedMessage + defaultMessage="event.definition_change.branch_replaced" + id="event.definition_change.branch_replaced" + values={ + Object { + "newBranch": <span + className="nowrap" + > + <LongLivingBranchIcon + className="little-spacer-left text-text-top" + /> + feature-y + </span>, + "oldBranch": <span + className="nowrap" + > + <LongLivingBranchIcon + className="little-spacer-left text-text-top" + /> + master + </span>, + "project": <Link + onClick={[Function]} + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": "feature-y", + "id": "bar", + }, + } + } + > + Bar + </Link>, + } + } + /> + </li> + </ul> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.tsx.snap index a8f48a01a16..b114a57e84a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.tsx.snap @@ -5,7 +5,7 @@ exports[`should render correctly the list of series 1`] = ` className="page-header" > <Select - className="input-medium pull-left big-spacer-right" + className="pull-left big-spacer-right input-medium" clearable={true} onChange={[Function]} optionComponent={[Function]} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css index ca57363cb5b..f95eaf09b8c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css @@ -182,6 +182,10 @@ color: #cccccc; } +.project-activity-event-icon.DEFINITION_CHANGE { + color: #33a759; +} + .project-activity-event-icon.OTHER { color: #442d1b; } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts index 1e8cc597218..ce069f69369 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts @@ -67,7 +67,7 @@ export interface MeasureHistory { } export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER']; -export const APPLICATION_EVENT_TYPES = ['QUALITY_GATE', 'OTHER']; +export const APPLICATION_EVENT_TYPES = ['QUALITY_GATE', 'DEFINITION_CHANGE', 'OTHER']; export const DEFAULT_GRAPH = 'issues'; export const GRAPH_TYPES = ['issues', 'coverage', 'duplications', 'custom']; export const GRAPHS_METRICS_DISPLAYED: { [x: string]: string[] } = { diff --git a/server/sonar-web/src/main/js/helpers/l10n.ts b/server/sonar-web/src/main/js/helpers/l10n.ts index 19014bcb8b0..dd2d8868403 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.ts +++ b/server/sonar-web/src/main/js/helpers/l10n.ts @@ -158,14 +158,14 @@ export function getLocalizedMetricName( short?: boolean ): string { const bundleKey = `metric.${metric.key}.${short ? 'short_name' : 'name'}`; - const fromBundle = translate(bundleKey); - if (fromBundle === bundleKey) { + if (hasMessage(bundleKey)) { + return translate(bundleKey); + } else { if (short) { return getLocalizedMetricName(metric); } return metric.name || metric.key; } - return fromBundle; } export function getLocalizedCategoryMetricName(metric: { key: string; name?: string }) { |