diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-18 17:47:37 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-25 11:05:36 +0200 |
commit | f6276b3b6fecce2b160ed8bdc62a3e87439249e4 (patch) | |
tree | d1c69e7f786b8693fb13dd816624794eb6b07ae7 /server/sonar-web | |
parent | 1ddf3ee7dbf26116afb767003a8a0698965c4f70 (diff) | |
download | sonarqube-f6276b3b6fecce2b160ed8bdc62a3e87439249e4.tar.gz sonarqube-f6276b3b6fecce2b160ed8bdc62a3e87439249e4.zip |
SONAR-9385 SONAR-9436 Replace moment with react-intl
Diffstat (limited to 'server/sonar-web')
91 files changed, 1104 insertions, 880 deletions
diff --git a/server/sonar-web/config/webpack.config.js b/server/sonar-web/config/webpack.config.js index e98dd7fa32a..c5d08949f53 100644 --- a/server/sonar-web/config/webpack.config.js +++ b/server/sonar-web/config/webpack.config.js @@ -32,7 +32,6 @@ module.exports = ({ production = true, fast = false }) => ({ 'react-dom', 'backbone', 'backbone.marionette', - 'moment', 'handlebars/runtime', './src/main/js/libs/third-party/jquery-ui.js', './src/main/js/libs/third-party/select2.js', diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index e9019ccbe63..d95aba437bc 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -20,16 +20,17 @@ "escape-html": "1.0.3", "handlebars": "2.0.0", "history": "3.3.0", + "intl-relativeformat": "2.0.0", "jquery": "2.2.0", "keymaster": "1.6.2", "lodash": "4.17.4", - "moment": "2.18.1", "numeral": "1.5.3", "rc-tooltip": "3.4.7", "react": "15.6.1", "react-dom": "15.6.1", "react-draggable": "2.2.6", "react-helmet": "5.1.3", + "react-intl": "2.3.0", "react-modal": "2.2.2", "react-redux": "5.0.5", "react-router": "3.0.5", @@ -52,6 +53,7 @@ "@types/react": "16.0.2", "@types/react-dom": "15.5.2", "@types/react-helmet": "5.0.3", + "@types/react-intl": "2.3.1", "@types/react-modal": "2.2.0", "@types/react-redux": "5.0.3", "@types/react-router": "3.0.5", diff --git a/server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx b/server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx new file mode 100644 index 00000000000..89e68eee611 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { addLocaleData, IntlProvider, Locale } from 'react-intl'; +import GlobalLoading from './GlobalLoading'; +import { DEFAULT_LANGUAGE, requestMessages } from '../../helpers/l10n'; + +interface Props { + children?: any; +} + +interface State { + loading: boolean; + lang?: string; +} + +export default class LocalizationContainer extends React.PureComponent<Props, State> { + mounted: boolean; + + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + requestMessages().then(this.bundleLoaded, this.bundleLoaded); + } + + componentWillUnmount() { + this.mounted = false; + } + + bundleLoaded = (lang: string) => { + import('react-intl/locale-data/' + (lang || DEFAULT_LANGUAGE)).then( + i => this.updateLang(lang, i), + () => { + import('react-intl/locale-data/en').then(i => this.updateLang(lang, i)); + } + ); + }; + + updateLang = (lang: string, intlBundle: Locale[]) => { + if (this.mounted) { + addLocaleData(intlBundle); + this.setState({ loading: false, lang }); + } + }; + + render() { + if (this.state.loading) { + return <GlobalLoading />; + } + return ( + <IntlProvider + locale={this.state.lang || DEFAULT_LANGUAGE} + defaultLocale={this.state.lang || DEFAULT_LANGUAGE}> + {this.props.children} + </IntlProvider> + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js index 811cf2b7397..0e21abc3301 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js @@ -24,7 +24,6 @@ import ComponentNavMeta from './ComponentNavMeta'; import ComponentNavMenu from './ComponentNavMenu'; import RecentHistory from '../../RecentHistory'; import ContextNavBar from '../../../../components/nav/ContextNavBar'; -import { TooltipsContainer } from '../../../../components/mixins/tooltips-mixin'; import { getTasksForComponent } from '../../../../api/ce'; import { STATUSES } from '../../../../apps/background-tasks/constants'; import './ComponentNav.css'; @@ -80,14 +79,12 @@ export default class ComponentNav extends React.PureComponent { breadcrumbs={this.props.component.breadcrumbs} /> - <TooltipsContainer options={{ delay: { show: 0, hide: 2000 } }}> - <ComponentNavMeta - {...this.props} - {...this.state} - version={this.props.component.version} - analysisDate={this.props.component.analysisDate} - /> - </TooltipsContainer> + <ComponentNavMeta + {...this.props} + {...this.state} + version={this.props.component.version} + analysisDate={this.props.component.analysisDate} + /> <ComponentNavMenu component={this.props.component} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js index 15b086e9b3c..28133dd41fe 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js @@ -17,10 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import React from 'react'; +import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter'; import IncrementalBadge from './IncrementalBadge'; import PendingIcon from '../../../../components/shared/pending-icon'; +import Tooltip from '../../../../components/controls/Tooltip'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; export default function ComponentNavMeta(props) { @@ -34,37 +35,51 @@ export default function ComponentNavMeta(props) { ? translateWithParameters('component_navigation.status.in_progress.admin', backgroundTasksUrl) : translate('component_navigation.status.in_progress'); metaList.push( - <li key="isInProgress" data-toggle="tooltip" title={tooltip}> - <i className="spinner" style={{ marginTop: '-1px' }} />{' '} - <span className="text-info">{translate('background_task.status.IN_PROGRESS')}</span> - </li> + <Tooltip + key="isInProgress" + overlay={<div dangerouslySetInnerHTML={{ __html: tooltip }} />} + mouseLeaveDelay={2}> + <li> + <i className="spinner" style={{ marginTop: '-1px' }} />{' '} + <span className="text-info">{translate('background_task.status.IN_PROGRESS')}</span> + </li> + </Tooltip> ); } else if (props.isPending) { const tooltip = canSeeBackgroundTasks ? translateWithParameters('component_navigation.status.pending.admin', backgroundTasksUrl) : translate('component_navigation.status.pending'); metaList.push( - <li key="isPending" data-toggle="tooltip" title={tooltip}> - <PendingIcon /> <span>{translate('background_task.status.PENDING')}</span> - </li> + <Tooltip + key="isPending" + overlay={<div dangerouslySetInnerHTML={{ __html: tooltip }} />} + mouseLeaveDelay={2}> + <li> + <PendingIcon /> <span>{translate('background_task.status.PENDING')}</span> + </li> + </Tooltip> ); } else if (props.isFailed) { const tooltip = canSeeBackgroundTasks ? translateWithParameters('component_navigation.status.failed.admin', backgroundTasksUrl) : translate('component_navigation.status.failed'); metaList.push( - <li key="isFailed" data-toggle="tooltip" title={tooltip}> - <span className="badge badge-danger"> - {translate('background_task.status.FAILED')} - </span> - </li> + <Tooltip + key="isFailed" + overlay={<div dangerouslySetInnerHTML={{ __html: tooltip }} />} + mouseLeaveDelay={2}> + <li> + <span className="badge badge-danger"> + {translate('background_task.status.FAILED')} + </span> + </li> + </Tooltip> ); } - if (props.analysisDate) { metaList.push( <li key="analysisDate"> - {moment(props.analysisDate).format('LLL')} + <DateTimeFormatter date={props.analysisDate} /> </li> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.js b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx index fd99e281ac9..e5f0a6ab6fa 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +import * as React from 'react'; import { shallow } from 'enzyme'; import ComponentNavMeta from '../ComponentNavMeta'; @@ -25,7 +25,7 @@ it('renders incremental badge', () => { check(true); check(false); - function check(incremental) { + function check(incremental: boolean) { expect( shallow( <ComponentNavMeta component={{ key: 'foo' }} conf={{}} incremental={incremental} /> diff --git a/server/sonar-web/src/main/js/app/utils/exposeLibraries.js b/server/sonar-web/src/main/js/app/utils/exposeLibraries.js index 7c458767be0..71635357dac 100644 --- a/server/sonar-web/src/main/js/app/utils/exposeLibraries.js +++ b/server/sonar-web/src/main/js/app/utils/exposeLibraries.js @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import * as ReactRedux from 'react-redux'; import * as ReactRouter from 'react-router'; import Select from 'react-select'; @@ -35,7 +34,6 @@ import DuplicationsRating from '../../components/ui/DuplicationsRating'; import Level from '../../components/ui/Level'; const exposeLibraries = () => { - window.moment = moment; window.ReactRedux = ReactRedux; window.ReactRouter = ReactRouter; window.SonarIcons = icons; diff --git a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx index b81b2cf256f..cd5f6d39fea 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx +++ b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx @@ -18,35 +18,39 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as moment from 'moment'; import { sortBy } from 'lodash'; +import { FormattedRelative } from 'react-intl'; import { Link } from 'react-router'; import { Project } from './types'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import Level from '../../../components/ui/Level'; +import Tooltip from '../../../components/controls/Tooltip'; import { translateWithParameters, translate } from '../../../helpers/l10n'; interface Props { project: Project; } -export default function ProjectCard(props: Props) { - const { project } = props; +export default function ProjectCard({ project }: Props) { const isAnalyzed = project.lastAnalysisDate != null; - const analysisMoment = isAnalyzed && moment(project.lastAnalysisDate); const links = sortBy(project.links, 'type'); return ( <div className="account-project-card clearfix"> <aside className="account-project-side"> {isAnalyzed - ? <div - className="account-project-analysis" - title={analysisMoment ? analysisMoment.format('LLL') : undefined}> - {translateWithParameters( - 'my_account.projects.analyzed_x', - analysisMoment ? analysisMoment.fromNow() : undefined - )} - </div> + ? <Tooltip + overlay={<DateTimeFormatter date={project.lastAnalysisDate} />} + placement="right"> + <div className="account-project-analysis"> + <FormattedRelative value={project.lastAnalysisDate}> + {(relativeDate: string) => + <span> + {translateWithParameters('my_account.projects.analyzed_x', relativeDate)} + </span>} + </FormattedRelative> + </div> + </Tooltip> : <div className="account-project-analysis"> {translate('my_account.projects.never_analyzed')} </div>} diff --git a/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js b/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js index f681f8af62f..aa597b1ce8c 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js +++ b/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js @@ -49,9 +49,7 @@ it('should not render optional fields', () => { it('should render analysis date', () => { const project = { ...BASE, lastAnalysisDate: '2016-05-17' }; const output = shallow(<ProjectCard project={project} />); - expect(output.find('.account-project-analysis').text()).toContain( - 'my_account.projects.analyzed_x' - ); + expect(output.find('.account-project-analysis FormattedRelative')).toHaveLength(1); }); it('should not render analysis date', () => { diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.js b/server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.js index 98bd76ece05..8fe7a69b028 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.js @@ -17,11 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow import $ from 'jquery'; -import moment from 'moment'; import React, { Component } from 'react'; -import { DATE_FORMAT } from '../constants'; +import { toShortNotSoISOString, isValidDate } from '../../../helpers/dates'; export default class DateFilter extends Component { componentDidMount() { @@ -47,17 +45,15 @@ export default class DateFilter extends Component { handleChange() { const date = {}; - const minDateRaw = this.refs.minDate.value; - const maxDateRaw = this.refs.maxDate.value; - const minDate = moment(minDateRaw, DATE_FORMAT, true); - const maxDate = moment(maxDateRaw, DATE_FORMAT, true); + const minDate = new Date(this.refs.minDate.value); + const maxDate = new Date(this.refs.maxDate.value); - if (minDate.isValid()) { - date.minSubmittedAt = minDate.format(DATE_FORMAT); + if (isValidDate(minDate)) { + date.minSubmittedAt = toShortNotSoISOString(minDate); } - if (maxDate.isValid()) { - date.maxExecutedAt = maxDate.format(DATE_FORMAT); + if (isValidDate(maxDate)) { + date.maxExecutedAt = toShortNotSoISOString(maxDate); } this.props.onChange(date); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Task.js b/server/sonar-web/src/main/js/apps/background-tasks/components/Task.js index 7f49e254a41..3c93857dadb 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Task.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Task.js @@ -48,9 +48,9 @@ export default class Task extends React.PureComponent { <TaskComponent task={task} /> <TaskId task={task} /> <TaskDay task={task} prevTask={prevTask} /> - <TaskDate date={task.submittedAt} baseDate={task.submittedAt} format="LTS" /> - <TaskDate date={task.startedAt} baseDate={task.submittedAt} format="LTS" /> - <TaskDate date={task.executedAt} baseDate={task.submittedAt} format="LTS" /> + <TaskDate date={task.submittedAt} baseDate={task.submittedAt} /> + <TaskDate date={task.startedAt} baseDate={task.submittedAt} /> + <TaskDate date={task.executedAt} baseDate={task.submittedAt} /> <TaskExecutionTime task={task} /> <TaskActions component={component} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx index 117702bafd9..d03a3e62289 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx @@ -17,28 +17,28 @@ * 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 moment from 'moment'; -import React from 'react'; +import * as React from 'react'; +import TimeFormatter from '../../../components/intl/TimeFormatter'; +import { differenceInDays, isValidDate } from '../../../helpers/dates'; -const TaskDate = ( - { date, baseDate, format } /*: { - date: string, - baseDate: string, - format: string -} */ -) => { - const m = moment(date); - const baseM = moment(baseDate); - const diff = date && baseDate ? m.diff(baseM, 'days') : 0; +interface Props { + date: string; + baseDate: string; +} + +export default function TaskDate({ date, baseDate }: Props) { + const parsedDate = new Date(date); + const parsedBaseDate = new Date(baseDate); + const diff = + date && baseDate && isValidDate(parsedDate) && isValidDate(parsedBaseDate) + ? differenceInDays(parsedDate, parsedBaseDate) + : 0; return ( <td className="thin nowrap text-right"> {diff > 0 && <span className="text-warning little-spacer-right">{`(+${diff}d)`}</span>} - {date ? moment(date).format(format) : ''} + {date && isValidDate(parsedDate) ? <TimeFormatter date={parsedDate} long={true} /> : ''} </td> ); -}; - -export default TaskDate; +} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx index 8307b341f36..ed6a79cef9c 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx @@ -17,23 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -/* @flow */ -import moment from 'moment'; -import React from 'react'; -/*:: import type { Task } from '../types'; */ +import * as React from 'react'; +import DateFormatter from '../../../components/intl/DateFormatter'; +import { isSameDay } from '../../../helpers/dates'; +import { ITask } from '../types'; -function isAnotherDay(a, b) { - return !moment(a).isSame(moment(b), 'day'); +interface Props { + task: ITask; + prevTask?: ITask; } -const TaskDay = ({ task, prevTask } /*: { task: Task, prevTask: ?Task } */) => { - const shouldDisplay = !prevTask || isAnotherDay(task.submittedAt, prevTask.submittedAt); +export default function TaskDay({ task, prevTask }: Props) { + const shouldDisplay = + !prevTask || !isSameDay(new Date(task.submittedAt), new Date(prevTask.submittedAt)); return ( <td className="thin nowrap text-right"> - {shouldDisplay ? moment(task.submittedAt).format('LL') : ''} + {shouldDisplay ? <DateFormatter date={task.submittedAt} long={true} /> : ''} </td> ); -}; - -export default TaskDay; +} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/types.js b/server/sonar-web/src/main/js/apps/background-tasks/types.ts index e272937867f..c517f201399 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/types.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/types.ts @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -/*:: -export type Task = { - incremental: boolean, - id: string -}; -*/ + +export interface ITask { + incremental: boolean; + id: string; + submittedAt: string; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js index ef72a1d213b..e8b41e80e36 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js @@ -20,7 +20,8 @@ // @flow import React from 'react'; import classNames from 'classnames'; -import moment from 'moment'; +import { FormattedRelative } from 'react-intl'; +import DateFormatter from '../../../components/intl/DateFormatter'; import Tooltip from '../../../components/controls/Tooltip'; import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -53,8 +54,13 @@ export default function LeakPeriodLegend({ className, component, period } /*: Pr } const date = getPeriodDate(period); - const fromNow = moment(date).fromNow(); - const tooltip = fromNow + ', ' + moment(date).format('LL'); + const tooltip = ( + <div> + <FormattedRelative value={date} /> + {', '} + <DateFormatter date={date} long={true} /> + </div> + ); return ( <Tooltip placement="left" overlay={tooltip}> {label} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js index b4c91f405cc..cf03a171d55 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import classNames from 'classnames'; -import moment from 'moment'; import Breadcrumbs from './Breadcrumbs'; import FilesView from '../drilldown/FilesView'; import MeasureFavoriteContainer from './MeasureFavoriteContainer'; @@ -217,15 +216,13 @@ export default class MeasureContent extends React.PureComponent { renderCode() { const { component, leakPeriod } = this.props; const leakPeriodDate = - isDiffMetric(this.props.metric.key) && leakPeriod != null - ? moment(leakPeriod.date).toDate() - : null; + isDiffMetric(this.props.metric.key) && leakPeriod != null ? new Date(leakPeriod.date) : null; let filterLine; if (leakPeriodDate != null) { filterLine = line => { if (line.scmDate) { - const scmDate = moment(line.scmDate).toDate(); + const scmDate = new Date(line.scmDate); return scmDate >= leakPeriodDate; } else { return false; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js index 4f7fce011d7..9becda572f1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js @@ -45,12 +45,6 @@ const PERIOD_DAYS = { parameter: '18' }; -jest.mock('moment', () => () => ({ - format: () => 'March 1, 2017 9:36 AM', - fromNow: () => 'a month ago', - toDate: () => 'date' -})); - it('should render correctly', () => { expect(shallow(<LeakPeriodLegend component={PROJECT} period={PERIOD} />)).toMatchSnapshot(); expect(shallow(<LeakPeriodLegend component={PROJECT} period={PERIOD_DAYS} />)).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap index 0f0e328d85e..b82411aaf92 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap @@ -2,7 +2,19 @@ exports[`should render correctly 1`] = ` <Tooltip - overlay="a month ago, March 1, 2017 9:36 AM" + overlay={ + <div> + <FormattedRelative + updateInterval={10000} + value={2017-05-16T11:50:02.000Z} + /> + , + <DateFormatter + date={2017-05-16T11:50:02.000Z} + long={true} + /> + </div> + } placement="left" > <div diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js index 3a30ccd1ec4..8a409a34ac8 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js @@ -19,13 +19,16 @@ */ // @flow import React from 'react'; -import moment from 'moment'; import { max } from 'lodash'; +import { FormattedRelative, intlShape } from 'react-intl'; +import { formatterOption, longFormatterOption } from '../../../components/intl/DateFormatter'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import FacetBox from '../../../components/facet/FacetBox'; import FacetHeader from '../../../components/facet/FacetHeader'; import FacetItem from '../../../components/facet/FacetItem'; import { BarChart } from '../../../components/charts/bar-chart'; import DateInput from '../../../components/controls/DateInput'; +import { isSameDay, toShortNotSoISOString } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; /*:: import type { Component } from '../utils'; */ @@ -46,8 +49,6 @@ type Props = {| |}; */ -const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ'; - export default class CreationDateFacet extends React.PureComponent { /*:: props: Props; */ @@ -55,6 +56,10 @@ export default class CreationDateFacet extends React.PureComponent { open: true }; + static contextTypes = { + intl: intlShape + }; + property = 'createdAt'; hasValue = () => @@ -84,36 +89,34 @@ export default class CreationDateFacet extends React.PureComponent { }; handleBarClick = ( - { - createdAfter, - createdBefore - } /*: { - createdAfter: Object, - createdBefore?: Object + { createdAfter, createdBefore } /*: { + createdAfter: Date, + createdBefore?: Date } */ ) => { this.resetTo({ - createdAfter: createdAfter.format(DATE_FORMAT), - createdBefore: createdBefore && createdBefore.format(DATE_FORMAT) + createdAfter: toShortNotSoISOString(createdAfter), + createdBefore: createdBefore && toShortNotSoISOString(createdBefore) }); }; - handlePeriodChange = (property /*: string */) => (value /*: string */) => { + handlePeriodChange = (property /*: string */, value /*: string */) => { this.props.onChange({ createdAt: undefined, createdInLast: undefined, sinceLeakPeriod: undefined, - [property]: value + [property]: toShortNotSoISOString(new Date(value)) }); }; - handlePeriodClick = (period /*: string */) => { - this.resetTo({ createdInLast: period }); - }; + handlePeriodChangeBefore = (value /*: string */) => + this.handlePeriodChange('createdBefore', value); - handleLeakPeriodClick = () => { - this.resetTo({ sinceLeakPeriod: true }); - }; + handlePeriodChangeAfter = (value /*: string */) => this.handlePeriodChange('createdAfter', value); + + handlePeriodClick = (period /*: string */) => this.resetTo({ createdInLast: period }); + + handleLeakPeriodClick = () => this.resetTo({ sinceLeakPeriod: true }); renderBarChart() { const { createdBefore, stats } = this.props; @@ -128,31 +131,32 @@ export default class CreationDateFacet extends React.PureComponent { return null; } - const data = periods.map((startDate, index) => { - const startMoment = moment(startDate); - const nextStartMoment = - index < periods.length - 1 - ? moment(periods[index + 1]) - : createdBefore ? moment(createdBefore) : undefined; - const endMoment = nextStartMoment && nextStartMoment.clone().subtract(1, 'days'); + const { formatDate } = this.context.intl; + const beforeDate = createdBefore ? createdBefore : undefined; + const data = periods.map((start, index) => { + const startDate = new Date(start); + let nextStartDate = index < periods.length - 1 ? periods[index + 1] : beforeDate; + let endDate; + if (nextStartDate) { + nextStartDate = new Date(nextStartDate); + endDate = new Date(nextStartDate); + endDate.setDate(endDate.getDate() - 1); + } let tooltip = - formatMeasure(stats[startDate], 'SHORT_INT') + '<br>' + startMoment.format('LL'); - - if (endMoment) { - const isSameDay = endMoment.diff(startMoment, 'days') <= 1; - if (!isSameDay) { - tooltip += ' – ' + endMoment.format('LL'); - } + formatMeasure(stats[start], 'SHORT_INT') + + '<br/>' + + formatDate(startDate, longFormatterOption); + if (endDate && !isSameDay(endDate, startDate)) { + tooltip += ' – ' + formatDate(endDate, longFormatterOption); } return { - createdAfter: startMoment, - createdBefore: nextStartMoment, - startMoment, + createdAfter: startDate, + createdBefore: nextStartDate, tooltip, x: index, - y: stats[startDate] + y: stats[start] }; }); @@ -177,13 +181,12 @@ export default class CreationDateFacet extends React.PureComponent { } renderExactDate() { - const m = moment(this.props.createdAt); return ( <div className="search-navigator-facet-container"> - {m.format('LLL')} + <DateTimeFormatter date={this.props.createdAt} /> <br /> <span className="note"> - ({m.fromNow()}) + <FormattedRelative value={this.props.createdAt} /> </span> </div> ); @@ -191,26 +194,26 @@ export default class CreationDateFacet extends React.PureComponent { renderPeriodSelectors() { const { createdAfter, createdBefore } = this.props; - + const { formatDate } = this.context.intl; return ( <div className="search-navigator-date-facet-selection"> <DateInput className="search-navigator-date-facet-selection-dropdown-left" - onChange={this.handlePeriodChange('createdAfter')} + onChange={this.handlePeriodChangeAfter} placeholder={translate('from')} - value={createdAfter ? moment(createdAfter).format('YYYY-MM-DD') : undefined} + value={createdAfter ? formatDate(createdAfter, formatterOption) : undefined} /> <DateInput className="search-navigator-date-facet-selection-dropdown-right" - onChange={this.handlePeriodChange('createdBefore')} + onChange={this.handlePeriodChangeBefore} placeholder={translate('to')} - value={createdBefore ? moment(createdBefore).format('YYYY-MM-DD') : undefined} + value={createdBefore ? formatDate(createdBefore, formatterOption) : undefined} /> </div> ); } - renderPrefefinedPeriods() { + renderPredefinedPeriods() { const { component, createdInLast, sinceLeakPeriod } = this.props; return ( <div className="spacer-top issues-predefined-periods"> @@ -259,7 +262,7 @@ export default class CreationDateFacet extends React.PureComponent { : <div> {this.renderBarChart()} {this.renderPeriodSelectors()} - {this.renderPrefefinedPeriods()} + {this.renderPredefinedPeriods()} </div>; } diff --git a/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js index 3c9785aa8be..aaca5381cb4 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js +++ b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter'; import { getApplicationLeak } from '../../../api/application'; import { translate } from '../../../helpers/l10n'; @@ -79,7 +79,7 @@ export default class ApplicationLeakPeriodLegend extends React.Component { ? <ul className="text-left"> {this.state.leaks.map(leak => <li key={leak.project}> - {leak.projectName}: <FormattedDate date={leak.date} format="LL" /> + {leak.projectName}: <DateTooltipFormatter date={leak.date} /> </li> )} </ul> diff --git a/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js index 88d26edef84..f932aeb8c8e 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js +++ b/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js @@ -19,7 +19,8 @@ */ // @flow import React from 'react'; -import moment from 'moment'; +import { FormattedRelative } from 'react-intl'; +import DateFormatter from '../../../components/intl/DateFormatter'; import Tooltip from '../../../components/controls/Tooltip'; import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; import { translateWithParameters } from '../../../helpers/l10n'; @@ -82,23 +83,33 @@ export default function LeakPeriodLegend({ period } /*: { period: Period } */) { } const leakPeriodDate = getPeriodDate(period); - const momentDate = moment(leakPeriodDate); - const fromNow = momentDate.fromNow(); - const note = ['date'].includes(period.mode) - ? translateWithParameters('overview.last_analysis_x', fromNow) - : translateWithParameters('overview.started_x', fromNow); - const tooltip = ['date'].includes(period.mode) - ? translateWithParameters('overview.last_analysis_on_x', momentDate.format('LL')) - : translateWithParameters('overview.started_on_x', momentDate.format('LL')); - + const tooltip = ( + <DateFormatter date={leakPeriodDate} long={true}> + {formattedLeakPeriodDate => + <span> + {translateWithParameters( + ['date'].includes(period.mode) + ? 'overview.last_analysis_on_x' + : 'overview.started_on_x', + formattedLeakPeriodDate + )} + </span>} + </DateFormatter> + ); return ( <Tooltip overlay={tooltip} placement="top"> <div className="overview-legend"> {translateWithParameters('overview.leak_period_x', leakPeriodLabel)} <br /> - <span className="note"> - {note} - </span> + <FormattedRelative value={leakPeriodDate}> + {fromNow => + <span className="note"> + {translateWithParameters( + ['date'].includes(period.mode) ? 'overview.last_analysis_x' : 'overview.started_x', + fromNow + )} + </span>} + </FormattedRelative> </div> </Tooltip> ); 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 index 11d09882375..800a62d4ccd 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import { uniq } from 'lodash'; -import moment from 'moment'; import QualityGate from '../qualityGate/QualityGate'; import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate'; import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities'; @@ -124,7 +123,7 @@ export default class OverviewApp extends React.PureComponent { const history /*: History */ = {}; r.measures.forEach(measure => { const measureHistory = measure.history.map(analysis => ({ - date: moment(analysis.date).toDate(), + date: new Date(analysis.date), value: analysis.value })); history[measure.metric] = measureHistory; diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js b/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js index 251e20594bc..b0179494ae0 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js @@ -37,7 +37,9 @@ describe('check note', () => { mode: 'date', parameter: '2013-01-01' }; - expect(shallow(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot(); + expect( + shallow(<LeakPeriodLegend period={period} />).find('FormattedRelative') + ).toMatchSnapshot(); }); it('version', () => { @@ -46,7 +48,9 @@ describe('check note', () => { mode: 'version', parameter: '0.1' }; - expect(shallow(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot(); + expect( + shallow(<LeakPeriodLegend period={period} />).find('FormattedRelative') + ).toMatchSnapshot(); }); it('previous_version', () => { @@ -54,7 +58,7 @@ describe('check note', () => { date: '2013-09-22T00:00:00+0200', mode: 'previous_version' }; - expect(shallow(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot(); + expect(shallow(<LeakPeriodLegend period={period} />).find('FormattedRelative')).toHaveLength(1); }); it('previous_analysis', () => { @@ -62,6 +66,6 @@ describe('check note', () => { date: '2013-09-22T00:00:00+0200', mode: 'previous_analysis' }; - expect(shallow(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot(); + expect(shallow(<LeakPeriodLegend period={period} />).find('FormattedRelative')).toHaveLength(1); }); }); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap index 217405a6565..cb842597822 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap @@ -28,17 +28,15 @@ exports[`renders 2`] = ` <li> Foo : - <FormattedDate + <DateTooltipFormatter date="2017-01-01T11:39:03+0100" - format="LL" /> </li> <li> Bar : - <FormattedDate + <DateTooltipFormatter date="2017-02-01T11:39:03+0100" - format="LL" /> </li> </ul> diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap index 397016097b3..44caddb1324 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap @@ -9,33 +9,15 @@ exports[`check note 10 days 1`] = ` `; exports[`check note date 1`] = ` -<span - className="note" -> - overview.last_analysis_x.4 years ago -</span> -`; - -exports[`check note previous_analysis 1`] = ` -<span - className="note" -> - overview.started_x.4 years ago -</span> -`; - -exports[`check note previous_version 1`] = ` -<span - className="note" -> - overview.started_x.4 years ago -</span> +<FormattedRelative + updateInterval={10000} + value={2013-09-21T22:00:00.000Z} +/> `; exports[`check note version 1`] = ` -<span - className="note" -> - overview.started_x.4 years ago -</span> +<FormattedRelative + updateInterval={10000} + value={2013-09-21T22:00:00.000Z} +/> `; 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 index ef1b6d37e8d..5a9f7cc7807 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js +++ b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js @@ -21,7 +21,7 @@ import React from 'react'; import { sortBy } from 'lodash'; import Event from './Event'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter'; import { translate } from '../../../helpers/l10n'; /*:: import type { Analysis as AnalysisType, Event as EventType } from '../../projectActivity/types'; */ @@ -46,7 +46,7 @@ export default function Analysis(props /*: Props */) { <li className="overview-analysis"> <div className="small little-spacer-bottom"> <strong> - <FormattedDate date={analysis.date} format="LL" /> + <DateTooltipFormatter date={analysis.date} placement="right" /> </strong> </div> 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 index be23bc7bc07..bf62a5bd1cc 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/Event.js +++ b/server/sonar-web/src/main/js/apps/overview/events/Event.js @@ -20,8 +20,8 @@ // @flow import React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; -/*:: import type { Event as EventType } from '../../projectActivity/types'; */ import { translate } from '../../../helpers/l10n'; +/*:: import type { Event as EventType } from '../../projectActivity/types'; */ export default function Event(props /*: { event: EventType } */) { const { event } = props; @@ -35,13 +35,17 @@ export default function Event(props /*: { event: EventType } */) { } return ( - <div className="overview-analysis-event"> + <span className="overview-analysis-event"> <span className="note">{translate('event.category', event.category)}:</span>{' '} - <Tooltip overlay={event.description} placement="left"> - <strong> - {event.name} - </strong> - </Tooltip> - </div> + {event.description + ? <Tooltip overlay={event.description} placement="left" mouseEnterDelay={0.5}> + <strong> + {event.name} + </strong> + </Tooltip> + : <strong> + {event.name} + </strong>} + </span> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js index d4bc3609a6d..c1a898a3bf1 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js @@ -19,7 +19,7 @@ */ import React from 'react'; import BubblePopup from '../../../components/common/BubblePopup'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateFormatter from '../../../components/intl/DateFormatter'; import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent'; /*:: import type { Metric } from '../types'; */ /*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */ @@ -56,7 +56,7 @@ export default class PreviewGraphTooltips extends React.PureComponent { <BubblePopup customClass={customClass} position={{ top, left, width: TOOLTIP_WIDTH }}> <div className="overview-analysis-graph-tooltip"> <div className="overview-analysis-graph-tooltip-title"> - <FormattedDate date={this.props.selectedDate} format="LL" /> + <DateFormatter date={this.props.selectedDate} long={true} /> </div> <table className="width-100"> <tbody> 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 index e37d21f47d9..33bc4e28c18 100644 --- 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 @@ -8,9 +8,9 @@ exports[`should sort the events with version first 1`] = ` className="small little-spacer-bottom" > <strong> - <FormattedDate + <DateTooltipFormatter date="2017-06-10T16:10:59+0200" - format="LL" + placement="right" /> </strong> </div> 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 index 18c6f76ec73..802c927a96c 100644 --- 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 @@ -9,7 +9,7 @@ exports[`should render a version correctly 1`] = ` `; exports[`should render an event correctly 1`] = ` -<div +<span className="overview-analysis-event" > <span @@ -19,12 +19,8 @@ exports[`should render an event correctly 1`] = ` : </span> - <Tooltip - placement="left" - > - <strong> - test - </strong> - </Tooltip> -</div> + <strong> + test + </strong> +</span> `; diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap index 0d0aec9012c..64d9d39a3e4 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap @@ -17,9 +17,9 @@ exports[`should render correctly 1`] = ` <div className="overview-analysis-graph-tooltip-title" > - <FormattedDate + <DateFormatter date={2011-10-25T10:27:41.000Z} - format="LL" + long={true} /> </div> <table diff --git a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js index 00e81e9d47a..2eab616e195 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js +++ b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js @@ -17,10 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import React from 'react'; import { Link } from 'react-router'; +import { FormattedRelative } from 'react-intl'; import Tooltip from '../../../components/controls/Tooltip'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import enhance from './enhance'; import { getMetricName } from '../helpers/metrics'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -43,9 +44,14 @@ class CodeSmells extends React.PureComponent { Object.assign(params, { sinceLeakPeriod: 'true' }); } - const formattedAnalysisDate = moment(component.analysisDate).format('LLL'); - const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate); - + const tooltip = ( + <DateTimeFormatter date={component.analysisDate}> + {formattedAnalysisDate => + <span> + {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)} + </span>} + </DateTimeFormatter> + ); return ( <Tooltip overlay={tooltip} placement="top"> <Link to={getComponentIssuesUrl(component.key, params)}> @@ -56,12 +62,16 @@ class CodeSmells extends React.PureComponent { } renderTimelineStartDate() { - const momentDate = moment(this.props.historyStartDate); - const fromNow = momentDate.fromNow(); + if (!this.props.historyStartDate) { + return null; + } return ( - <span className="overview-domain-timeline-date"> - {translateWithParameters('overview.started_x', fromNow)} - </span> + <FormattedRelative value={this.props.historyStartDate}> + {fromNow => + <span className="overview-domain-timeline-date"> + {translateWithParameters('overview.started_x', fromNow)} + </span>} + </FormattedRelative> ); } 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 index b4be68adcb1..fb008cabb12 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js @@ -19,9 +19,9 @@ */ import React from 'react'; import { Link } from 'react-router'; -import moment from 'moment'; 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'; @@ -157,8 +157,16 @@ export default function enhance(ComposedComponent) { if (isDiffMetric(metric)) { Object.assign(params, { sinceLeakPeriod: 'true' }); } - const formattedAnalysisDate = moment(component.analysisDate).format('LLL'); - const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate); + + const tooltip = ( + <DateTimeFormatter date={component.analysisDate}> + {formattedAnalysisDate => + <span> + {translateWithParameters('widget.as_calculated_on_x', formattedAnalysisDate)} + </span>} + </DateTimeFormatter> + ); + return ( <Tooltip overlay={tooltip} placement="top"> <Link to={getComponentIssuesUrl(component.key, params)}> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap index 06b8994c23d..1bc54255ce1 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap @@ -4,11 +4,11 @@ exports[`generateCoveredLinesMetric should correctly generate covered lines metr Object { "data": Array [ Object { - "x": 2017-04-27T06:21:32.000Z, + "x": 2017-04-27T08:21:32.000Z, "y": 88, }, Object { - "x": 2017-04-30T21:06:24.000Z, + "x": 2017-04-30T23:06:24.000Z, "y": 50, }, ], @@ -23,11 +23,11 @@ Array [ Object { "data": Array [ Object { - "x": 2017-04-27T06:21:32.000Z, + "x": 2017-04-27T08:21:32.000Z, "y": 88, }, Object { - "x": 2017-04-30T21:06:24.000Z, + "x": 2017-04-30T23:06:24.000Z, "y": 50, }, ], @@ -38,11 +38,11 @@ Array [ Object { "data": Array [ Object { - "x": 2017-04-27T06:21:32.000Z, + "x": 2017-04-27T08:21:32.000Z, "y": 100, }, Object { - "x": 2017-04-30T21:06:24.000Z, + "x": 2017-04-30T23:06:24.000Z, "y": 100, }, ], @@ -57,9 +57,9 @@ exports[`getAnalysesByVersionByDay should also filter analysis based on the quer Array [ Object { "byDay": Object { - "2017-4-18": Array [ + "1495065600000": Array [ Object { - "date": 2017-05-18T12:13:07.000Z, + "date": 2017-05-18T14:13:07.000Z, "events": Array [ Object { "category": "QUALITY_PROFILE", @@ -76,9 +76,9 @@ Array [ }, Object { "byDay": Object { - "2017-4-16": Array [ + "1494892800000": Array [ Object { - "date": 2017-05-16T05:09:59.000Z, + "date": 2017-05-16T07:09:59.000Z, "events": Array [ Object { "category": "VERSION", @@ -105,9 +105,9 @@ exports[`getAnalysesByVersionByDay should also filter analysis based on the quer Array [ Object { "byDay": Object { - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [], "key": "AVyM9n3cHjR_PLDzRciT", }, @@ -118,9 +118,9 @@ Array [ }, Object { "byDay": Object { - "2017-4-18": Array [ + "1495065600000": Array [ Object { - "date": 2017-05-18T12:13:07.000Z, + "date": 2017-05-18T14:13:07.000Z, "events": Array [ Object { "category": "QUALITY_PROFILE", @@ -131,9 +131,9 @@ Array [ "key": "AVxZtCpH7841nF4RNEMI", }, ], - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [ Object { "category": "VERSION", @@ -160,9 +160,9 @@ exports[`getAnalysesByVersionByDay should correctly map analysis by versions and Array [ Object { "byDay": Object { - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T11:06:10.000Z, + "date": 2017-06-09T13:06:10.000Z, "events": Array [ Object { "category": "VERSION", @@ -173,7 +173,7 @@ Array [ "key": "AVyMjlK1HjR_PLDzRbB9", }, Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [], "key": "AVyM9n3cHjR_PLDzRciT", }, @@ -184,9 +184,9 @@ Array [ }, Object { "byDay": Object { - "2017-4-18": Array [ + "1495065600000": Array [ Object { - "date": 2017-05-18T12:13:07.000Z, + "date": 2017-05-18T14:13:07.000Z, "events": Array [ Object { "category": "QUALITY_PROFILE", @@ -197,14 +197,14 @@ Array [ "key": "AVxZtCpH7841nF4RNEMI", }, Object { - "date": 2017-05-18T05:17:32.000Z, + "date": 2017-05-18T07:17:32.000Z, "events": Array [], "key": "AVwaa1qkpbBde8B6UhYI", }, ], - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [ Object { "category": "VERSION", @@ -221,9 +221,16 @@ Array [ }, Object { "byDay": Object { - "2017-4-16": Array [ + "1494288000000": Array [ Object { - "date": 2017-05-16T05:09:59.000Z, + "date": 2017-05-09T12:03:59.000Z, + "events": Array [], + "key": "AVvtGF3IY6vCuQNDdwxI", + }, + ], + "1494892800000": Array [ + Object { + "date": 2017-05-16T07:09:59.000Z, "events": Array [ Object { "category": "VERSION", @@ -239,13 +246,6 @@ Array [ "key": "AVwQF7kwl-nNFgFWOJ3V", }, ], - "2017-4-9": Array [ - Object { - "date": 2017-05-09T10:03:59.000Z, - "events": Array [], - "key": "AVvtGF3IY6vCuQNDdwxI", - }, - ], }, "key": "AVyM9oI1HjR_PLDzRciU", "version": "1.0", @@ -257,26 +257,26 @@ exports[`getAnalysesByVersionByDay should create fake version 1`] = ` Array [ Object { "byDay": Object { - "2017-4-18": Array [ + "1495065600000": Array [ Object { - "date": 2017-05-18T12:13:07.000Z, + "date": 2017-05-18T14:13:07.000Z, "events": Array [], "key": "AVxZtCpH7841nF4RNEMI", }, ], - "2017-5-9": Array [ + "1496966400000": Array [ Object { - "date": 2017-06-09T11:06:10.000Z, + "date": 2017-06-09T13:06:10.000Z, "events": Array [], "key": "AVyMjlK1HjR_PLDzRbB9", }, Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [], "key": "AVyM9n3cHjR_PLDzRciT", }, Object { - "date": 2017-06-09T09:12:27.000Z, + "date": 2017-06-09T11:12:27.000Z, "events": Array [], "key": "AVyMjlK1HjR_PLDzRbB9", }, diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js index 2ee3b2cfe3d..455c679d2d7 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js @@ -19,22 +19,23 @@ */ // @flow import * as utils from '../utils'; +import * as dates from '../../../helpers/dates'; const ANALYSES = [ { key: 'AVyMjlK1HjR_PLDzRbB9', - date: new Date('2017-06-09T13:06:10+0200'), + date: new Date('2017-06-09T13:06:10.000Z'), events: [{ key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.1-SNAPSHOT' }] }, - { key: 'AVyM9n3cHjR_PLDzRciT', date: new Date('2017-06-09T11:12:27+0200'), events: [] }, + { key: 'AVyM9n3cHjR_PLDzRciT', date: new Date('2017-06-09T11:12:27.000Z'), events: [] }, { key: 'AVyMjlK1HjR_PLDzRbB9', - date: new Date('2017-06-09T11:12:27+0200'), + date: new Date('2017-06-09T11:12:27.000Z'), events: [{ key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.1' }] }, { key: 'AVxZtCpH7841nF4RNEMI', - date: new Date('2017-05-18T14:13:07+0200'), + date: new Date('2017-05-18T14:13:07.000Z'), events: [ { key: 'AVxZtC-N7841nF4RNEMJ', @@ -43,10 +44,10 @@ const ANALYSES = [ } ] }, - { key: 'AVwaa1qkpbBde8B6UhYI', date: new Date('2017-05-18T07:17:32+0200'), events: [] }, + { key: 'AVwaa1qkpbBde8B6UhYI', date: new Date('2017-05-18T07:17:32.000Z'), events: [] }, { key: 'AVwQF7kwl-nNFgFWOJ3V', - date: new Date('2017-05-16T07:09:59+0200'), + date: new Date('2017-05-16T07:09:59.000Z'), events: [ { key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.0' }, { @@ -56,22 +57,22 @@ const ANALYSES = [ } ] }, - { key: 'AVvtGF3IY6vCuQNDdwxI', date: new Date('2017-05-09T12:03:59+0200'), events: [] } + { key: 'AVvtGF3IY6vCuQNDdwxI', date: new Date('2017-05-09T12:03:59.000Z'), events: [] } ]; const HISTORY = [ { metric: 'lines_to_cover', history: [ - { date: new Date('2017-04-27T08:21:32+0200'), value: '100' }, - { date: new Date('2017-04-30T23:06:24+0200'), value: '100' } + { date: new Date('2017-04-27T08:21:32.000Z'), value: '100' }, + { date: new Date('2017-04-30T23:06:24.000Z'), value: '100' } ] }, { metric: 'uncovered_lines', history: [ - { date: new Date('2017-04-27T08:21:32+0200'), value: '12' }, - { date: new Date('2017-04-30T23:06:24+0200'), value: '50' } + { date: new Date('2017-04-27T08:21:32.000Z'), value: '12' }, + { date: new Date('2017-04-30T23:06:24.000Z'), value: '50' } ] } ]; @@ -83,7 +84,7 @@ const METRICS = [ const QUERY = { category: '', - from: new Date('2017-04-27T08:21:32+0200'), + from: new Date('2017-04-27T08:21:32.000Z'), graph: utils.DEFAULT_GRAPH, project: 'foo', to: undefined, @@ -91,16 +92,6 @@ const QUERY = { customMetrics: ['foo', 'bar', 'baz'] }; -jest.mock('moment', () => date => ({ - startOf: () => { - return { - valueOf: () => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` - }; - }, - toDate: () => new Date(date), - format: format => `Formated.${format}:${date.valueOf()}` -})); - describe('generateCoveredLinesMetric', () => { it('should correctly generate covered lines metric', () => { expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY)).toMatchSnapshot(); @@ -116,6 +107,12 @@ describe('generateSeries', () => { }); describe('getAnalysesByVersionByDay', () => { + dates.startOfDay = jest.fn(date => { + const startDay = new Date(date); + startDay.setUTCHours(0, 0, 0, 0); + return startDay; + }); + it('should correctly map analysis by versions and by days', () => { expect( utils.getAnalysesByVersionByDay(ANALYSES, { @@ -141,8 +138,8 @@ describe('getAnalysesByVersionByDay', () => { customMetrics: [], graph: utils.DEFAULT_GRAPH, project: 'foo', - to: new Date('2017-06-09T11:12:27+0200'), - from: new Date('2017-05-18T14:13:07+0200') + to: new Date('2017-06-09T11:12:27.000Z'), + from: new Date('2017-05-18T14:13:07.000Z') }) ).toMatchSnapshot(); }); @@ -150,10 +147,10 @@ describe('getAnalysesByVersionByDay', () => { expect( utils.getAnalysesByVersionByDay( [ - { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T13:06:10+0200'), events: [] }, - { key: 'AVyM9n3cHjR_PLDzRciT', date: new Date('2017-06-09T11:12:27+0200'), events: [] }, - { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T11:12:27+0200'), events: [] }, - { key: 'AVxZtCpH7841nF4RNEMI', date: new Date('2017-05-18T14:13:07+0200'), events: [] } + { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T13:06:10.000Z'), events: [] }, + { key: 'AVyM9n3cHjR_PLDzRciT', date: new Date('2017-06-09T11:12:27.000Z'), events: [] }, + { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T11:12:27.000Z'), events: [] }, + { key: 'AVxZtCpH7841nF4RNEMI', date: new Date('2017-05-18T14:13:07.000Z'), events: [] } ], { category: '', @@ -208,7 +205,7 @@ describe('parseQuery', () => { it('should parse query with default values', () => { expect( utils.parseQuery({ - from: '2017-04-27T08:21:32+0200', + from: '2017-04-27T08:21:32.000Z', id: 'foo', custom_metrics: 'foo,bar,baz' }) @@ -219,11 +216,11 @@ describe('parseQuery', () => { describe('serializeQuery', () => { it('should serialize query for api request', () => { expect(utils.serializeQuery(QUERY)).toEqual({ - from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000', + from: '2017-04-27T08:21:32+0000', project: 'foo' }); expect(utils.serializeQuery({ ...QUERY, graph: 'coverage', category: 'test' })).toEqual({ - from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000', + from: '2017-04-27T08:21:32+0000', project: 'foo', category: 'test' }); @@ -233,14 +230,14 @@ describe('serializeQuery', () => { describe('serializeUrlQuery', () => { it('should serialize query for url', () => { expect(utils.serializeUrlQuery(QUERY)).toEqual({ - from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000', + from: '2017-04-27T08:21:32+0000', id: 'foo', custom_metrics: 'foo,bar,baz' }); expect( utils.serializeUrlQuery({ ...QUERY, graph: 'coverage', category: 'test', customMetrics: [] }) ).toEqual({ - from: 'Formated.YYYY-MM-DDTHH:mm:ssZZ:1493274092000', + from: '2017-04-27T08:21:32+0000', id: 'foo', graph: 'coverage', category: 'test' @@ -256,8 +253,8 @@ describe('hasHistoryData', () => { name: 'foo', type: 'INT', data: [ - { x: new Date('2017-04-27T08:21:32+0200'), y: 2 }, - { x: new Date('2017-04-30T23:06:24+0200'), y: 2 } + { x: new Date('2017-04-27T08:21:32.000Z'), y: 2 }, + { x: new Date('2017-04-30T23:06:24.000Z'), y: 2 } ] } ]) @@ -273,8 +270,8 @@ describe('hasHistoryData', () => { name: 'bar', type: 'INT', data: [ - { x: new Date('2017-04-27T08:21:32+0200'), y: 2 }, - { x: new Date('2017-04-30T23:06:24+0200'), y: 2 } + { x: new Date('2017-04-27T08:21:32.000Z'), y: 2 }, + { x: new Date('2017-04-30T23:06:24.000Z'), y: 2 } ] } ]) @@ -284,7 +281,7 @@ describe('hasHistoryData', () => { { name: 'bar', type: 'INT', - data: [{ x: new Date('2017-04-27T08:21:32+0200'), y: 2 }] + data: [{ x: new Date('2017-04-27T08:21:32.000Z'), y: 2 }] } ]) ).toBeFalsy(); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js index 4e5be7ea5db..044b1462323 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import moment from 'moment'; import { isEqual, sortBy } from 'lodash'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import GraphHistory from './GraphHistory'; @@ -87,7 +86,7 @@ export default class GraphsHistory extends React.PureComponent { return acc.concat({ className: event.category, name: event.name, - date: moment(analysis.date).toDate() + date: new Date(analysis.date) }); }, []); return sortBy(filteredEvents, 'date'); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js index 4eb0e719a28..320f934e028 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import BubblePopup from '../../../components/common/BubblePopup'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import GraphsTooltipsContent from './GraphsTooltipsContent'; import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents'; import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage'; @@ -98,7 +98,7 @@ export default class GraphsTooltips extends React.PureComponent { <BubblePopup customClass={customClass} position={{ top, left, width: TOOLTIP_WIDTH }}> <div className="project-activity-graph-tooltip"> <div className="project-activity-graph-tooltip-title spacer-bottom"> - <FormattedDate date={this.props.selectedDate} format="LL" /> + <DateTimeFormatter date={this.props.selectedDate} /> </div> <table className="width-100"> <tbody> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js index 7f69288c33d..7b94b01483c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js @@ -20,10 +20,9 @@ // @flow import React from 'react'; import classNames from 'classnames'; -import moment from 'moment'; import { throttle } from 'lodash'; import ProjectActivityAnalysis from './ProjectActivityAnalysis'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import DateFormatter from '../../../components/intl/DateFormatter'; import { translate } from '../../../helpers/l10n'; import { activityQueryChanged, @@ -191,12 +190,9 @@ export default class ProjectActivityAnalysesList extends React.PureComponent { </div>} <ul className="project-activity-days-list"> {days.map(day => - <li - key={day} - className="project-activity-day" - data-day={moment(Number(day)).format('YYYY-MM-DD')}> + <li key={day} className="project-activity-day"> <div className="project-activity-date"> - <FormattedDate date={Number(day)} format="LL" /> + <DateFormatter date={Number(day)} long={true} /> </div> <ul className="project-activity-analyses-list"> {version.byDay[day] != null && diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js index f52a59b5943..bc17b6a46b6 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js @@ -23,7 +23,7 @@ import classNames from 'classnames'; import Events from './Events'; import AddEventForm from './forms/AddEventForm'; import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; -import FormattedDate from '../../../components/ui/FormattedDate'; +import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter'; import { translate } from '../../../helpers/l10n'; /*:: import type { Analysis } from '../types'; */ @@ -65,7 +65,7 @@ export default class ProjectActivityAnalysis extends React.PureComponent { role="listitem" tabIndex="0"> <div className="project-activity-time spacer-right"> - <FormattedDate className="text-middle" date={date} format="LT" tooltipFormat="LTS" /> + <TimeTooltipFormatter className="text-middle" date={date} placement="right" /> </div> <div className="project-activity-analysis-icon big-spacer-right" title={analysisTitle} /> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js index cdc41b48e99..2de698d4b52 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js @@ -20,7 +20,6 @@ // @flow import React from 'react'; import Helmet from 'react-helmet'; -import moment from 'moment'; import ProjectActivityPageHeader from './ProjectActivityPageHeader'; import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; import ProjectActivityGraphs from './ProjectActivityGraphs'; @@ -89,7 +88,7 @@ export default function ProjectActivityApp(props /*: Props */) { <div className="project-activity-layout-page-main"> <ProjectActivityGraphs analyses={analyses} - leakPeriodDate={moment(props.project.leakPeriodDate).toDate()} + leakPeriodDate={new Date(props.project.leakPeriodDate)} loading={props.graphLoading} measuresHistory={measuresHistory} metrics={props.metrics} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index 61e9bd077c2..172e0d35bcf 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -19,7 +19,6 @@ */ // @flow import React from 'react'; -import moment from 'moment'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import ProjectActivityApp from './ProjectActivityApp'; @@ -173,7 +172,7 @@ class ProjectActivityAppContainer extends React.PureComponent { return api .getProjectActivity({ ...parameters, ...additional }) .then(({ analyses, paging }) => ({ - analyses: analyses.map(analysis => ({ ...analysis, date: moment(analysis.date).toDate() })), + analyses: analyses.map(analysis => ({ ...analysis, date: new Date(analysis.date) })), paging })); }; @@ -187,7 +186,7 @@ class ProjectActivityAppContainer extends React.PureComponent { measures.map(measure => ({ metric: measure.metric, history: measure.history.map(analysis => ({ - date: moment(analysis.date).toDate(), + date: new Date(analysis.date), value: analysis.value })) })), diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js index 1ece1355970..f6f85c29c9b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js @@ -19,8 +19,9 @@ */ // @flow import React from 'react'; -import moment from 'moment'; +import { intlShape } from 'react-intl'; import DateInput from '../../../components/controls/DateInput'; +import { formatterOption } from '../../../components/intl/DateFormatter'; import { parseAsDate } from '../../../helpers/query'; import { translate } from '../../../helpers/l10n'; /*:: import type { RawQuery } from '../../../helpers/query'; */ @@ -36,13 +37,18 @@ type Props = { export default class ProjectActivityDateInput extends React.PureComponent { /*:: props: Props; */ + static contextTypes = { + intl: intlShape + }; + handleFromDateChange = (from /*: string */) => this.props.onChange({ from: parseAsDate(from) }); handleToDateChange = (to /*: string */) => this.props.onChange({ to: parseAsDate(to) }); handleResetClick = () => this.props.onChange({ from: null, to: null }); - formatDate = (date /*: ?Date */) => (date ? moment(date).format('YYYY-MM-DD') : null); + formatDate = (date /*: ?Date */) => + date ? this.context.intl.formatDate(date, formatterOption) : undefined; render() { return ( diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js index 30864a995f9..6a0c61741f2 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js @@ -20,6 +20,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import ProjectActivityAnalysesList from '../ProjectActivityAnalysesList'; +import * as dates from '../../../../helpers/dates'; import { DEFAULT_GRAPH } from '../../utils'; const ANALYSES = [ @@ -83,18 +84,14 @@ const DEFAULT_PROPS = { updateQuery: () => {} }; -jest.mock('moment', () => date => ({ - startOf: () => { - return { - valueOf: () => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` - }; - }, - toDate: () => new Date(date), - format: format => `Formated.${format}:${date}` -})); - window.Number = val => val; +dates.startOfDay = jest.fn(date => { + const startDay = new Date(date); + startDay.setUTCHours(0, 0, 0, 0); + return startDay; +}); + it('should render correctly', () => { expect(shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />)).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js index fa199664561..6b93eaae296 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js @@ -18,12 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallowWithIntl } from '../../../../helpers/testUtils'; import ProjectActivityDateInput from '../ProjectActivityDateInput'; it('should render correctly the date inputs', () => { expect( - shallow( + shallowWithIntl( <ProjectActivityDateInput from={new Date('2016-10-27T12:21:15+0000')} to={new Date('2016-12-27T12:21:15+0000')} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap index aea593d413e..e2ab4b039a8 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap @@ -17,9 +17,8 @@ exports[`should not add separators if not needed 1`] = ` <div className="project-activity-graph-tooltip-title spacer-bottom" > - <FormattedDate + <DateTimeFormatter date={2011-10-01T22:01:00.000Z} - format="LL" /> </div> <table @@ -53,9 +52,8 @@ exports[`should render correctly for issues graphs 1`] = ` <div className="project-activity-graph-tooltip-title spacer-bottom" > - <FormattedDate + <DateTimeFormatter date={2011-10-01T22:01:00.000Z} - format="LL" /> </div> <table @@ -109,9 +107,8 @@ exports[`should render correctly for random graphs 1`] = ` <div className="project-activity-graph-tooltip-title spacer-bottom" > - <FormattedDate + <DateTimeFormatter date={2011-10-25T10:27:41.000Z} - format="LL" /> </div> <table diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap index 32622085444..00938d611a8 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap @@ -25,14 +25,13 @@ exports[`should correctly filter analyses by category 1`] = ` > <li className="project-activity-day" - data-day="Formated.YYYY-MM-DD:2016-9-24" > <div className="project-activity-date" > - <FormattedDate - date="2016-9-24" - format="LL" + <DateFormatter + date="1477267200000" + long={true} /> </div> <ul @@ -95,14 +94,13 @@ exports[`should correctly filter analyses by date range 1`] = ` > <li className="project-activity-day" - data-day="Formated.YYYY-MM-DD:2016-9-27" > <div className="project-activity-date" > - <FormattedDate - date="2016-9-27" - format="LL" + <DateFormatter + date="1477526400000" + long={true} /> </div> <ul @@ -165,14 +163,13 @@ exports[`should render correctly 1`] = ` > <li className="project-activity-day" - data-day="Formated.YYYY-MM-DD:2016-9-27" > <div className="project-activity-date" > - <FormattedDate - date="2016-9-27" - format="LL" + <DateFormatter + date="1477526400000" + long={true} /> </div> <ul @@ -241,14 +238,13 @@ exports[`should render correctly 1`] = ` > <li className="project-activity-day" - data-day="Formated.YYYY-MM-DD:2016-9-26" > <div className="project-activity-date" > - <FormattedDate - date="2016-9-26" - format="LL" + <DateFormatter + date="1477440000000" + long={true} /> </div> <ul @@ -288,14 +284,13 @@ exports[`should render correctly 1`] = ` </li> <li className="project-activity-day" - data-day="Formated.YYYY-MM-DD:2016-9-24" > <div className="project-activity-date" > - <FormattedDate - date="2016-9-24" - format="LL" + <DateFormatter + date="1477267200000" + long={true} /> </div> <ul diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap index bc15b05c60e..95d05668fef 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap @@ -8,7 +8,7 @@ exports[`should render correctly the date inputs 1`] = ` name="from" onChange={[Function]} placeholder="from" - value="2016-10-27" + value="10/27/2016" /> — <DateInput @@ -17,7 +17,7 @@ exports[`should render correctly the date inputs 1`] = ` name="to" onChange={[Function]} placeholder="to" - value="2016-12-27" + value="12/27/2016" /> <button className="spacer-left" diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js index 12694dba2bf..03eb0510b61 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import moment from 'moment'; import { chunk, flatMap, groupBy, isEqual, sortBy } from 'lodash'; import { cleanQuery, @@ -29,6 +28,7 @@ import { serializeDate, serializeString } from '../../helpers/query'; +import { startOfDay } from '../../helpers/dates'; import { getLocalizedMetricName, translate } from '../../helpers/l10n'; /*:: import type { Analysis, MeasureHistory, Metric, Query } from './types'; */ /*:: import type { RawQuery } from '../../helpers/query'; */ @@ -157,7 +157,7 @@ export function getAnalysesByVersionByDay(analyses /*: Array<Analysis> */, query acc.push(currentVersion); } - const day = moment(analysis.date).startOf('day').valueOf().toString(); + const day = startOfDay(new Date(analysis.date)).getTime().toString(); let matchFilters = true; if (query.category || query.from || query.to) { diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.js index b6af4d5cd10..695136bb6c6 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.js @@ -20,8 +20,9 @@ // @flow import React from 'react'; import classNames from 'classnames'; -import moment from 'moment'; import { Link } from 'react-router'; +import { FormattedRelative } from 'react-intl'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter.tsx'; import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardLeakMeasures from './ProjectCardLeakMeasures'; import FavoriteContainer from '../../../components/controls/FavoriteContainer'; @@ -90,19 +91,19 @@ export default function ProjectCardLeak({ measures, organization, project } /*: hasLeakPeriodStart && <div className="project-card-dates note text-right pull-right"> {hasLeakPeriodStart && - <span className="project-card-leak-date pull-right"> - {translateWithParameters( - 'projects.leak_period_x', - moment(project.leakPeriodDate).fromNow() - )} - </span>} + <FormattedRelative value={project.leakPeriodDate}> + {fromNow => + <span className="project-card-leak-date pull-right"> + {translateWithParameters('projects.leak_period_x', fromNow)} + </span>} + </FormattedRelative>} {isProjectAnalyzed && - <span> - {translateWithParameters( - 'projects.last_analysis_on_x', - moment(project.analysisDate).format('LLL') - )} - </span>} + <DateTimeFormatter date={project.analysisDate}> + {formattedDate => + <span> + {translateWithParameters('projects.last_analysis_on_x', formattedDate)} + </span>} + </DateTimeFormatter>} </div>} </div> diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.js index 5d8cddae025..baea9ad442e 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.js @@ -20,8 +20,8 @@ // @flow import React from 'react'; import classNames from 'classnames'; -import moment from 'moment'; import { Link } from 'react-router'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardOverallMeasures from './ProjectCardOverallMeasures'; import FavoriteContainer from '../../../components/controls/FavoriteContainer'; @@ -87,12 +87,12 @@ export default function ProjectCardOverall({ measures, organization, project } / </div> {isProjectAnalyzed && <div className="project-card-dates note text-right"> - <span className="big-spacer-left"> - {translateWithParameters( - 'projects.last_analysis_on_x', - moment(project.analysisDate).format('LLL') - )} - </span> + <DateTimeFormatter date={project.analysisDate}> + {formattedDate => + <span className="big-spacer-left"> + {translateWithParameters('projects.last_analysis_on_x', formattedDate)} + </span>} + </DateTimeFormatter> </div>} </div> diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.js index 46819f2bd13..2ac25e044ab 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.js +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.js @@ -35,15 +35,11 @@ const MEASURES = { new_bugs: 12 }; -jest.mock('moment', () => () => ({ - format: () => 'March 1, 2017 9:36 AM', - fromNow: () => 'a month ago' -})); - it('should display analysis date and leak start date', () => { const card = shallow(<ProjectCardLeak type="leak" measures={MEASURES} project={PROJECT} />); expect(card.find('.project-card-dates').exists()).toBeTruthy(); - expect(card.find('.project-card-dates').find('span').getNodes()).toHaveLength(2); + expect(card.find('.project-card-dates').find('FormattedRelative').getNodes()).toHaveLength(1); + expect(card.find('.project-card-dates').find('DateTimeFormatter').getNodes()).toHaveLength(1); }); it('should not display analysis date or leak start date', () => { diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.js index b6f5698f231..91144662ec5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.js +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.js @@ -35,11 +35,6 @@ const MEASURES = { new_bugs: 12 }; -jest.mock('moment', () => () => ({ - format: () => 'March 1, 2017 9:36 AM', - fromNow: () => 'a month ago' -})); - it('should display analysis date (and not leak period) when defined', () => { expect( shallow(<ProjectCardOverall measures={{}} project={PROJECT} />) diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.js.snap index cb539fbafe6..591ed49e1c8 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.js.snap @@ -35,14 +35,13 @@ exports[`should display the leak measures and quality gate 1`] = ` <div className="project-card-dates note text-right pull-right" > - <span - className="project-card-leak-date pull-right" - > - projects.leak_period_x.a month ago - </span> - <span> - projects.last_analysis_on_x.March 1, 2017 9:36 AM - </span> + <FormattedRelative + updateInterval={10000} + value="2016-12-01" + /> + <DateTimeFormatter + date="2017-01-01" + /> </div> </div> <div diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.js.snap index b93bf252c31..720f0f20129 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.js.snap @@ -35,11 +35,9 @@ exports[`should display the overall measures and quality gate 1`] = ` <div className="project-card-dates note text-right" > - <span - className="big-spacer-left" - > - projects.last_analysis_on_x.March 1, 2017 9:36 AM - </span> + <DateTimeFormatter + date="2017-01-01" + /> </div> </div> <div diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx index 1dbfdda95fd..1e94f724259 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx @@ -19,10 +19,11 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import * as moment from 'moment'; import ChangesList from './ChangesList'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import { translate } from '../../../helpers/l10n'; import { getRulesUrl } from '../../../helpers/urls'; +import { differenceInSeconds } from '../../../helpers/dates'; import { ProfileChangelogEvent } from '../types'; interface Props { @@ -35,7 +36,8 @@ export default function Changelog(props: Props) { const rows = props.events.map((event, index) => { const prev = index > 0 ? props.events[index - 1] : null; - const isSameDate = prev != null && moment(prev.date).diff(event.date, 'seconds') < 10; + const isSameDate = + prev != null && differenceInSeconds(new Date(prev.date), new Date(event.date)) < 10; const isBulkChange = prev != null && isSameDate && @@ -51,7 +53,7 @@ export default function Changelog(props: Props) { return ( <tr key={index} className={className}> <td className="thin nowrap"> - {!isBulkChange && moment(event.date).format('LLL')} + {!isBulkChange && <DateTimeFormatter date={event.date} />} </td> <td className="thin nowrap"> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx index b28f8e236e0..6272bcc2f12 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx @@ -45,7 +45,7 @@ it('should render events', () => { it('should render event date', () => { const events = [createEvent()]; const changelog = shallow(<Changelog events={events} organization={null} />); - expect(changelog.text()).toContain('2016'); + expect(changelog.find('DateTimeFormatter')).toHaveLength(1); }); it('should render author', () => { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx index 947b15e419c..3841c70ee0f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx @@ -18,7 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as moment from 'moment'; +import { FormattedRelative } from 'react-intl'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -27,9 +29,11 @@ interface Props { export default function ProfileDate({ date }: Props) { return date - ? <span title={moment(date).format('LLL')} data-toggle="tooltip"> - {moment(date).fromNow()} - </span> + ? <Tooltip overlay={<DateTimeFormatter date={date} />}> + <span> + <FormattedRelative value={date} /> + </span> + </Tooltip> : <span> {translate('never')} </span>; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx index 84df54ceb49..2119c23ae3e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx @@ -19,17 +19,15 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import * as moment from 'moment'; import { sortBy } from 'lodash'; import { searchRules } from '../../../api/rules'; import { translateWithParameters, translate } from '../../../helpers/l10n'; import { getRulesUrl } from '../../../helpers/urls'; +import { toShortNotSoISOString } from '../../../helpers/dates'; import { formatMeasure } from '../../../helpers/measures'; const RULES_LIMIT = 10; -const PERIOD_START_MOMENT = moment().subtract(1, 'year'); - function parseRules(r: any) { const { rules, actives } = r; return rules.map((rule: any) => { @@ -55,8 +53,16 @@ interface State { } export default class EvolutionRules extends React.PureComponent<Props, State> { + periodStartDate: string; mounted: boolean; - state: State = {}; + + constructor(props: Props) { + super(props); + this.state = {}; + const startDate = new Date(); + startDate.setFullYear(startDate.getFullYear() - 1); + this.periodStartDate = toShortNotSoISOString(startDate); + } componentDidMount() { this.mounted = true; @@ -69,7 +75,7 @@ export default class EvolutionRules extends React.PureComponent<Props, State> { loadLatestRules() { const data = { - available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD'), + available_since: this.periodStartDate, s: 'createdAt', asc: false, ps: RULES_LIMIT, @@ -92,9 +98,7 @@ export default class EvolutionRules extends React.PureComponent<Props, State> { } const newRulesUrl = getRulesUrl( - { - available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD') - }, + { available_since: this.periodStartDate }, this.props.organization ); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx index 578020e7129..5252d1c1840 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as moment from 'moment'; +import DateFormatter from '../../../components/intl/DateFormatter'; import ProfileLink from '../components/ProfileLink'; -import { translate } from '../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; import { isStagnant } from '../utils'; import { Profile } from '../types'; @@ -29,7 +29,7 @@ interface Props { profiles: Profile[]; } -export default function EvolutionStagnan(props: Props) { +export default function EvolutionStagnant(props: Props) { // TODO filter built-in out const outdated = props.profiles.filter(isStagnant); @@ -60,11 +60,16 @@ export default function EvolutionStagnan(props: Props) { {profile.name} </ProfileLink> </div> - <div className="note"> - {profile.languageName} - {', '} - updated on {moment(profile.rulesUpdatedAt).format('LL')} - </div> + <DateFormatter date={profile.rulesUpdatedAt} long={true}> + {formattedDate => + <div className="note"> + {translateWithParameters( + 'quality_profiles.x_updated_on_y', + profile.languageName, + formattedDate + )} + </div>} + </DateFormatter> </li> )} </ul> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts index cc7140c0fd4..d058aa01acd 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-profiles/utils.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { sortBy } from 'lodash'; -import * as moment from 'moment'; +import { differenceInYears, isValidDate } from '../../helpers/dates'; import { Profile } from './types'; export function sortProfiles(profiles: Profile[]) { @@ -65,8 +65,14 @@ export function createFakeProfile(overrides?: any) { }; } -export function isStagnant(profile: Profile) { - return moment().diff(moment(profile.userUpdatedAt), 'years') >= 1; +export function isStagnant(profile: Profile): boolean { + if (profile.userUpdatedAt) { + const updateDate = new Date(profile.userUpdatedAt); + if (isValidDate(updateDate)) { + return differenceInYears(new Date(), updateDate) >= 1; + } + } + return false; } export const getProfilesPath = (organization: string | null | undefined) => diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js index f8ef6d4c7f0..d6b26fd3a75 100644 --- a/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js +++ b/server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js @@ -19,7 +19,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import moment from 'moment'; +import DateFormatter from '../../../components/intl/DateFormatter'; import LicenseStatus from './LicenseStatus'; import LicenseChangeForm from './LicenseChangeForm'; @@ -50,7 +50,7 @@ export default class LicenseRow extends React.PureComponent { <td className="js-expiration text-middle"> {license.expiration != null && <div className={license.invalidExpiration ? 'text-danger' : null}> - {moment(license.expiration).format('LL')} + <DateFormatter date={license.expiration} long={true} /> </div>} </td> <td className="js-type text-middle"> diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js index 4d7c758d416..94e3d55b633 100644 --- a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js +++ b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js @@ -73,7 +73,7 @@ it('should render expiration', () => { '.js-expiration' ); expect(licenseExpiration.length).toBe(1); - expect(licenseExpiration.text()).toContain('2015'); + expect(licenseExpiration.find('DateFormatter')).toHaveLength(1); }); it('should render invalid expiration', () => { diff --git a/server/sonar-web/src/main/js/components/charts/Timeline.js b/server/sonar-web/src/main/js/components/charts/Timeline.js deleted file mode 100644 index 0896127d099..00000000000 --- a/server/sonar-web/src/main/js/components/charts/Timeline.js +++ /dev/null @@ -1,237 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 $ from 'jquery'; -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import { extent, max } from 'd3-array'; -import { scaleLinear, scalePoint, scaleTime } from 'd3-scale'; -import { line as d3Line, curveBasis } from 'd3-shape'; -import { ResizeMixin } from '../mixins/resize-mixin'; -import { TooltipsMixin } from '../mixins/tooltips-mixin'; - -const Timeline = createReactClass({ - displayName: 'Timeline', - - propTypes: { - data: PropTypes.arrayOf(PropTypes.object).isRequired, - padding: PropTypes.arrayOf(PropTypes.number), - height: PropTypes.number, - basisCurve: PropTypes.bool - }, - - mixins: [ResizeMixin, TooltipsMixin], - - getDefaultProps() { - return { - padding: [10, 10, 10, 10], - basisCurve: true - }; - }, - - getInitialState() { - return { - width: this.props.width, - height: this.props.height - }; - }, - - getRatingScale(availableHeight) { - return scalePoint().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]); - }, - - getLevelScale(availableHeight) { - return scalePoint().domain(['ERROR', 'WARN', 'OK']).range([availableHeight, 0]); - }, - - getYScale(availableHeight) { - if (this.props.metricType === 'RATING') { - return this.getRatingScale(availableHeight); - } else if (this.props.metricType === 'LEVEL') { - return this.getLevelScale(availableHeight); - } else { - return scaleLinear() - .range([availableHeight, 0]) - .domain([0, max(this.props.data, d => d.y || 0)]) - .nice(); - } - }, - - handleEventMouseEnter(event) { - $(`.js-event-circle-${event.date.getTime()}`).tooltip('show'); - }, - - handleEventMouseLeave(event) { - $(`.js-event-circle-${event.date.getTime()}`).tooltip('hide'); - }, - - renderHorizontalGrid(xScale, yScale) { - const hasTicks = typeof yScale.ticks === 'function'; - const ticks = hasTicks ? yScale.ticks(4) : yScale.domain(); - - if (!ticks.length) { - ticks.push(yScale.domain()[1]); - } - - const grid = ticks.map(tick => { - const opts = { - x: xScale.range()[0], - y: yScale(tick) - }; - - return ( - <g key={tick}> - <text - className="line-chart-tick line-chart-tick-x" - dx="-1em" - dy="0.3em" - textAnchor="end" - {...opts}> - {this.props.formatYTick(tick)} - </text> - <line - className="line-chart-grid" - x1={xScale.range()[0]} - x2={xScale.range()[1]} - y1={yScale(tick)} - y2={yScale(tick)} - /> - </g> - ); - }); - - return ( - <g> - {grid} - </g> - ); - }, - - renderTicks(xScale, yScale) { - const format = xScale.tickFormat(7); - let ticks = xScale.ticks(7); - - ticks = ticks.slice(0, -1).map((tick, index) => { - const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1]; - const x = (xScale(tick) + xScale(nextTick)) / 2; - const y = yScale.range()[0]; - - return ( - <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em"> - {format(tick)} - </text> - ); - }); - - return ( - <g> - {ticks} - </g> - ); - }, - - renderLeak(xScale, yScale) { - if (!this.props.leakPeriodDate) { - return null; - } - - const yScaleRange = yScale.range(); - const opts = { - x: xScale(this.props.leakPeriodDate), - y: yScaleRange[yScaleRange.length - 1], - width: xScale.range()[1] - xScale(this.props.leakPeriodDate), - height: yScaleRange[0] - yScaleRange[yScaleRange.length - 1], - fill: '#fbf3d5' - }; - - return <rect {...opts} />; - }, - - renderLine(xScale, yScale) { - const p = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y)); - if (this.props.basisCurve) { - p.curve(curveBasis); - } - return <path className="line-chart-path" d={p(this.props.data)} />; - }, - - renderEvents(xScale, yScale) { - const points = this.props.events - .map(event => { - const snapshot = this.props.data.find(d => d.x.getTime() === event.date.getTime()); - return { ...event, snapshot }; - }) - .filter(event => event.snapshot) - .map(event => { - const key = `${event.date.getTime()}-${event.snapshot.y}`; - const className = `line-chart-point js-event-circle-${event.date.getTime()}`; - const value = event.snapshot.y ? this.props.formatValue(event.snapshot.y) : '—'; - const tooltip = [ - `<span class="nowrap">${event.version}</span>`, - `<span class="nowrap">${moment(event.date).format('LL')}</span>`, - `<span class="nowrap">${value}</span>` - ].join('<br>'); - return ( - <circle - key={key} - className={className} - r="4" - cx={xScale(event.snapshot.x)} - cy={yScale(event.snapshot.y)} - onMouseEnter={this.handleEventMouseEnter.bind(this, event)} - onMouseLeave={this.handleEventMouseLeave.bind(this, event)} - data-toggle="tooltip" - data-title={tooltip} - /> - ); - }); - return ( - <g> - {points} - </g> - ); - }, - - render() { - if (!this.state.width || !this.state.height) { - return <div />; - } - const availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3]; - const availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2]; - const xScale = scaleTime() - .domain(extent(this.props.data, d => d.x || 0)) - .range([0, availableWidth]) - .clamp(true); - const yScale = this.getYScale(availableHeight); - return ( - <svg className="line-chart" width={this.state.width} height={this.state.height}> - <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}> - {this.renderLeak(xScale, yScale)} - {this.renderHorizontalGrid(xScale, yScale)} - {this.renderTicks(xScale, yScale)} - {this.renderLine(xScale, yScale)} - {this.renderEvents(xScale, yScale)} - </g> - </svg> - ); - } -}); -export default Timeline; diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx index a1f4a4e70d9..33f6c7d9cf8 100644 --- a/server/sonar-web/src/main/js/components/controls/DateInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/DateInput.tsx @@ -45,7 +45,7 @@ export default class DateInput extends React.PureComponent<Props> { } componentWillReceiveProps(nextProps: Props) { - if (nextProps.value != null) { + if (nextProps.value != null && this.input) { this.input.value = nextProps.value; } } @@ -63,8 +63,8 @@ export default class DateInput extends React.PureComponent<Props> { onSelect: this.handleChange.bind(this) }; - if ($.fn && ($.fn as any).datepicker) { - ($(this.refs.input) as any).datepicker(opts); + if ($.fn && ($.fn as any).datepicker && this.input) { + ($(this.input) as any).datepicker(opts); } } diff --git a/server/sonar-web/src/main/js/components/intl/DateFormatter.tsx b/server/sonar-web/src/main/js/components/intl/DateFormatter.tsx new file mode 100644 index 00000000000..0c789489587 --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/DateFormatter.tsx @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { DateSource, FormattedDate } from 'react-intl'; + +interface Props { + children?: (formattedDate: string) => React.ReactNode; + date: DateSource; + long?: boolean; +} + +export const formatterOption = { year: 'numeric', month: '2-digit', day: '2-digit' }; + +export const longFormatterOption = { year: 'numeric', month: 'long', day: 'numeric' }; + +export default function DateFormatter({ children, date, long }: Props) { + return ( + <FormattedDate + children={children} + value={date} + {...(long ? longFormatterOption : formatterOption)} + /> + ); +} diff --git a/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx b/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx new file mode 100644 index 00000000000..560270c23e6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { DateSource, FormattedDate } from 'react-intl'; + +interface Props { + children?: (formattedDate: string) => React.ReactNode; + date: DateSource; +} + +export const formatterOption = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric' +}; + +export default function DateTimeFormatter({ children, date }: Props) { + return <FormattedDate children={children} value={date} {...formatterOption} />; +} diff --git a/server/sonar-web/src/main/js/components/ui/FormattedDate.js b/server/sonar-web/src/main/js/components/intl/DateTooltipFormatter.tsx index f25d00307f3..f8c7e90233a 100644 --- a/server/sonar-web/src/main/js/components/ui/FormattedDate.js +++ b/server/sonar-web/src/main/js/components/intl/DateTooltipFormatter.tsx @@ -17,34 +17,29 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import moment from 'moment'; +import * as React from 'react'; +import DateFormatter from './DateFormatter'; +import DateTimeFormatter from './DateTimeFormatter'; +import Tooltip from '../controls/Tooltip'; -export default class FormattedDate extends React.PureComponent { - /*:: props: { - className?: string, - date: string | number, - format?: string, - tooltipFormat?: string - }; -*/ - - static defaultProps = { - format: 'LLL' - }; - - render() { - const { className, date, format, tooltipFormat } = this.props; - - const m = moment(date); - - const title = tooltipFormat ? m.format(tooltipFormat) : undefined; +interface Props { + className?: string; + date: Date | string | number; + placement?: string; +} - return ( - <time className={className} dateTime={m.format()} title={title}> - {m.format(format)} - </time> - ); - } +export default function DateTooltipFormatter({ className, date, placement }: Props) { + return ( + <DateFormatter date={date} long={true}> + {formattedDate => + <Tooltip + overlay={<DateTimeFormatter date={date} />} + placement={placement} + mouseEnterDelay={0.5}> + <time className={className} dateTime={new Date(date as Date).toISOString()}> + {formattedDate} + </time> + </Tooltip>} + </DateFormatter> + ); } diff --git a/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx b/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx new file mode 100644 index 00000000000..9823aa8a9ce --- /dev/null +++ b/server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { DateSource, FormattedTime } from 'react-intl'; + +interface Props { + children?: (formattedDate: string) => React.ReactNode; + date: DateSource; + long?: boolean; +} + +export const formatterOption = { hour: 'numeric', minute: 'numeric' }; + +export const longFormatterOption = { hour: 'numeric', minute: 'numeric', second: 'numeric' }; + +export default function TimeFormatter({ children, date, long }: Props) { + return ( + <FormattedTime + children={children} + value={date} + {...(long ? longFormatterOption : formatterOption)} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/LocalizationContainer.js b/server/sonar-web/src/main/js/components/intl/TimeTooltipFormatter.tsx index 2144033ef9d..4ee3edc783e 100644 --- a/server/sonar-web/src/main/js/app/components/LocalizationContainer.js +++ b/server/sonar-web/src/main/js/components/intl/TimeTooltipFormatter.tsx @@ -17,37 +17,28 @@ * 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 GlobalLoading from './GlobalLoading'; -import { requestMessages } from '../../helpers/l10n'; +import * as React from 'react'; +import TimeFormatter from './TimeFormatter'; +import Tooltip from '../controls/Tooltip'; -export default class LocalizationContainer extends React.PureComponent { - /*:: mounted: boolean; */ - - state = { - loading: true - }; - - componentDidMount() { - this.mounted = true; - requestMessages().then(this.finishLoading, this.finishLoading); - } - - componentWillUnmount() { - this.mounted = false; - } - - finishLoading = () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }; +interface Props { + className?: string; + date: Date | string | number; + placement?: string; +} - render() { - if (this.state.loading) { - return <GlobalLoading />; - } - return this.props.children; - } +export default function TimeTooltipFormatter({ className, date, placement }: Props) { + return ( + <TimeFormatter date={date} long={false}> + {formattedTime => + <Tooltip + overlay={<TimeFormatter date={date} long={true} />} + placement={placement} + mouseEnterDelay={0.5}> + <time className={className} dateTime={new Date(date as Date).toISOString()}> + {formattedTime} + </time> + </Tooltip>} + </TimeFormatter> + ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js index 721c83ff7e4..404e395479a 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js @@ -19,9 +19,11 @@ */ // @flow import React from 'react'; -import moment from 'moment'; +import { FormattedRelative } from 'react-intl'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import ChangelogPopup from '../popups/ChangelogPopup'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import Tooltip from '../../../components/controls/Tooltip'; /*:: import type { Issue } from '../types'; */ /*:: @@ -47,22 +49,25 @@ export default class IssueChangelog extends React.PureComponent { }; render() { - const momentCreationDate = moment(this.props.creationDate); return ( <BubblePopupHelper isOpen={this.props.isOpen} position="bottomright" togglePopup={this.toggleChangelog} popup={<ChangelogPopup issue={this.props.issue} onFail={this.props.onFail} />}> - <button - className="button-link issue-action issue-action-with-options js-issue-show-changelog" - title={momentCreationDate.format('LLL')} - onClick={this.handleClick}> - <span className="issue-meta-label"> - {momentCreationDate.fromNow()} - </span> - <i className="icon-dropdown little-spacer-left" /> - </button> + <Tooltip + overlay={<DateTimeFormatter date={this.props.creationDate} />} + placement="left" + mouseEnterDelay={0.5}> + <button + className="button-link issue-action issue-action-with-options js-issue-show-changelog" + onClick={this.handleClick}> + <span className="issue-meta-label"> + <FormattedRelative value={this.props.creationDate} /> + </span> + <i className="icon-dropdown little-spacer-left" /> + </button> + </Tooltip> </BubblePopupHelper> ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js index 54836503b39..51865ad5850 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js @@ -19,7 +19,7 @@ */ // @flow import React from 'react'; -import moment from 'moment'; +import { FormattedRelative } from 'react-intl'; import Avatar from '../../../components/ui/Avatar'; import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; import CommentDeletePopup from '../popups/CommentDeletePopup'; @@ -98,7 +98,7 @@ export default class IssueCommentLine extends React.PureComponent { tabIndex={0} /> <div className="issue-comment-age"> - ({moment(comment.createdAt).fromNow()}) + <FormattedRelative value={comment.createdAt} /> </div> <div className="issue-comment-actions"> {comment.updatable && diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js index 446dc8bcea4..65c2c001f3a 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js @@ -28,11 +28,6 @@ const issue = { creationDate: '2017-03-01T09:36:01+0100' }; -jest.mock('moment', () => () => ({ - format: () => 'March 1, 2017 9:36 AM', - fromNow: () => 'a month ago' -})); - it('should render correctly', () => { const element = shallow( <IssueChangelog diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js index 9096b729386..3075e60cc89 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js @@ -31,8 +31,6 @@ const comment = { updatable: true }; -jest.mock('moment', () => () => ({ fromNow: () => 'a month ago' })); - it('should render correctly a comment that is not updatable', () => { const element = shallow( <IssueCommentLine diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap index 8e1f9850fc8..531d5bf7ab2 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap @@ -27,20 +27,32 @@ exports[`should open the popup when the button is clicked 2`] = ` position="bottomright" togglePopup={[Function]} > - <button - className="button-link issue-action issue-action-with-options js-issue-show-changelog" - onClick={[Function]} - title="March 1, 2017 9:36 AM" + <Tooltip + mouseEnterDelay={0.5} + overlay={ + <DateTimeFormatter + date="2017-03-01T09:36:01+0100" + /> + } + placement="left" > - <span - className="issue-meta-label" + <button + className="button-link issue-action issue-action-with-options js-issue-show-changelog" + onClick={[Function]} > - a month ago - </span> - <i - className="icon-dropdown little-spacer-left" - /> - </button> + <span + className="issue-meta-label" + > + <FormattedRelative + updateInterval={10000} + value="2017-03-01T09:36:01+0100" + /> + </span> + <i + className="icon-dropdown little-spacer-left" + /> + </button> + </Tooltip> </BubblePopupHelper> `; @@ -62,19 +74,31 @@ exports[`should render correctly 1`] = ` position="bottomright" togglePopup={[Function]} > - <button - className="button-link issue-action issue-action-with-options js-issue-show-changelog" - onClick={[Function]} - title="March 1, 2017 9:36 AM" + <Tooltip + mouseEnterDelay={0.5} + overlay={ + <DateTimeFormatter + date="2017-03-01T09:36:01+0100" + /> + } + placement="left" > - <span - className="issue-meta-label" + <button + className="button-link issue-action issue-action-with-options js-issue-show-changelog" + onClick={[Function]} > - a month ago - </span> - <i - className="icon-dropdown little-spacer-left" - /> - </button> + <span + className="issue-meta-label" + > + <FormattedRelative + updateInterval={10000} + value="2017-03-01T09:36:01+0100" + /> + </span> + <i + className="icon-dropdown little-spacer-left" + /> + </button> + </Tooltip> </BubblePopupHelper> `; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap index 9d4ea6fa3aa..a3b7b63d015 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap @@ -42,9 +42,10 @@ exports[`should open the right popups when the buttons are clicked 3`] = ` <div className="issue-comment-age" > - ( - a month ago - ) + <FormattedRelative + updateInterval={10000} + value="2017-03-01T09:36:01+0100" + /> </div> <div className="issue-comment-actions" @@ -140,9 +141,10 @@ exports[`should render correctly a comment that is not updatable 1`] = ` <div className="issue-comment-age" > - ( - a month ago - ) + <FormattedRelative + updateInterval={10000} + value="2017-03-01T09:36:01+0100" + /> </div> <div className="issue-comment-actions" @@ -180,9 +182,10 @@ exports[`should render correctly a comment that is updatable 1`] = ` <div className="issue-comment-age" > - ( - a month ago - ) + <FormattedRelative + updateInterval={10000} + value="2017-03-01T09:36:01+0100" + /> </div> <div className="issue-comment-actions" diff --git a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js index 6f0ca9ac054..01549d5dac7 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js @@ -19,11 +19,11 @@ */ // @flow import React from 'react'; -import moment from 'moment'; import { getIssueChangelog } from '../../../api/issues'; import { translate } from '../../../helpers/l10n'; import Avatar from '../../../components/ui/Avatar'; import BubblePopup from '../../../components/common/BubblePopup'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import IssueChangelogDiff from '../components/IssueChangelogDiff'; /*:: import type { ChangelogDiff } from '../components/IssueChangelogDiff'; */ /*:: import type { Issue } from '../types'; */ @@ -86,7 +86,7 @@ export default class ChangelogPopup extends React.PureComponent { <tbody> <tr> <td className="thin text-left text-top nowrap"> - {moment(issue.creationDate).format('LLL')} + <DateTimeFormatter date={issue.creationDate} /> </td> <td className="text-left text-top"> {author ? `${translate('created_by')} ${author}` : translate('created')} @@ -96,7 +96,7 @@ export default class ChangelogPopup extends React.PureComponent { {this.state.changelogs.map((item, idx) => <tr key={idx}> <td className="thin text-left text-top nowrap"> - {moment(item.creationDate).format('LLL')} + <DateTimeFormatter date={item.creationDate} /> </td> <td className="text-left text-top"> {item.userName && diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js index 35d5c05b5f2..6c4f9d5977e 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js @@ -21,8 +21,6 @@ import { shallow } from 'enzyme'; import React from 'react'; import ChangelogPopup from '../ChangelogPopup'; -jest.mock('moment', () => () => ({ format: () => 'March 1, 2017 9:36 AM' })); - it('should render the changelog popup correctly', () => { const element = shallow( <ChangelogPopup diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap index 078a01fc7bf..1e649d62abb 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap @@ -15,7 +15,9 @@ exports[`should render the changelog popup correctly 1`] = ` <td className="thin text-left text-top nowrap" > - March 1, 2017 9:36 AM + <DateTimeFormatter + date="2017-03-01T09:36:01+0100" + /> </td> <td className="text-left text-top" @@ -27,7 +29,9 @@ exports[`should render the changelog popup correctly 1`] = ` <td className="thin text-left text-top nowrap" > - March 1, 2017 9:36 AM + <DateTimeFormatter + date="2017-03-01T09:36:01+0100" + /> </td> <td className="text-left text-top" diff --git a/server/sonar-web/src/main/js/components/widgets/barchart.js b/server/sonar-web/src/main/js/components/widgets/barchart.js index 670068ed228..a75ee83fe44 100644 --- a/server/sonar-web/src/main/js/components/widgets/barchart.js +++ b/server/sonar-web/src/main/js/components/widgets/barchart.js @@ -18,17 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import $ from 'jquery'; -import moment from 'moment'; import { max } from 'd3-array'; import { select } from 'd3-selection'; import { scaleLinear, scaleBand } from 'd3-scale'; +import { isSameDay, toNotSoISOString } from '../../helpers/dates'; function trans(left, top) { return `translate(${left}, ${top})`; } -const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ'; - const defaults = function() { return { height: 140, @@ -53,7 +51,7 @@ $.fn.barchart = function(data) { const options = { ...defaults(), ...$(this).data() }; Object.assign(options, { width: options.width || $(this).width(), - endDate: options.endDate ? moment(options.endDate) : null + endDate: options.endDate ? new Date(options.endDate) : null }); const container = select(this); @@ -93,26 +91,28 @@ $.fn.barchart = function(data) { .attr('width', barWidth) .attr('height', d => Math.floor(yScale(d.count))) .style('cursor', 'pointer') - .attr('data-period-start', d => moment(d.val).format(DATE_FORMAT)) + .attr('data-period-start', d => toNotSoISOString(new Date(d.val))) .attr('data-period-end', (d, i) => { - const ending = i < data.length - 1 ? moment(data[i + 1].val) : options.endDate; + const ending = i < data.length - 1 ? new Date(data[i + 1].val) : options.endDate; if (ending) { - return ending.format(DATE_FORMAT); + return toNotSoISOString(ending); } else { return ''; } }) .attr('title', (d, i) => { - const beginning = moment(d.val); - const ending = - i < data.length - 1 ? moment(data[i + 1].val).subtract(1, 'days') : options.endDate; + const beginning = new Date(d.val); + let ending = options.endDate; + if (i < data.length - 1) { + ending = new Date(data[i + 1].val); + ending.setDate(ending.getDate() - 1); + } if (ending) { - const isSameDay = ending.diff(beginning, 'days') <= 1; return ( d.text + '<br>' + beginning.format('LL') + - (isSameDay ? '' : ' – ' + ending.format('LL')) + (isSameDay(ending, beginning) ? '' : ' – ' + ending.format('LL')) ); } else { return d.text + '<br>' + beginning.format('LL'); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts new file mode 100644 index 00000000000..455886d15a8 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 dates from '../dates'; + +const recentDate = new Date('2017-08-16T12:00:00.000Z'); +const recentDate2 = new Date('2016-12-16T12:00:00.000Z'); +const oldDate = new Date('2014-01-12T12:00:00.000Z'); + +it('toShortNotSoISOString', () => { + expect(dates.toShortNotSoISOString(recentDate)).toBe('2017-08-16'); +}); + +it('toNotSoISOString', () => { + expect(dates.toNotSoISOString(recentDate)).toBe('2017-08-16T12:00:00+0000'); +}); + +it('startOfDay', () => { + expect(dates.startOfDay(recentDate).toTimeString()).toContain('00:00:00'); + expect(dates.startOfDay(recentDate)).not.toBe(recentDate); +}); + +it('isValidDate', () => { + expect(dates.isValidDate(recentDate)).toBeTruthy(); + expect(dates.isValidDate(new Date())).toBeTruthy(); + expect(dates.isValidDate(new Date('foo'))).toBeFalsy(); +}); + +it('isSameDay', () => { + expect(dates.isSameDay(recentDate, new Date(recentDate))).toBeTruthy(); + expect(dates.isSameDay(recentDate, recentDate2)).toBeFalsy(); + expect(dates.isSameDay(recentDate, oldDate)).toBeFalsy(); + expect(dates.isSameDay(recentDate, new Date('2016-08-16T12:00:00.000Z'))).toBeFalsy(); +}); + +it('differenceInYears', () => { + expect(dates.differenceInYears(recentDate, recentDate2)).toBe(0); + expect(dates.differenceInYears(recentDate, oldDate)).toBe(3); + expect(dates.differenceInYears(oldDate, recentDate)).toBe(-3); +}); + +it('differenceInDays', () => { + expect(dates.differenceInDays(recentDate, new Date('2017-08-01T12:00:00.000Z'))).toBe(15); + expect(dates.differenceInDays(recentDate, new Date('2017-08-15T23:00:00.000Z'))).toBe(0); + expect(dates.differenceInDays(recentDate, recentDate2)).toBe(243); + expect(dates.differenceInDays(recentDate, oldDate)).toBe(1312); +}); + +it('differenceInSeconds', () => { + expect(dates.differenceInSeconds(recentDate, new Date('2017-08-16T10:00:00.000Z'))).toBe(7200); + expect(dates.differenceInSeconds(recentDate, new Date('2017-08-16T12:00:00.500Z'))).toBe(0); + expect(dates.differenceInSeconds(recentDate, oldDate)).toBe(113356800); +}); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts index 3763be42db6..3763be42db6 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts diff --git a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js index 982f9375a36..11d7b289cae 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import * as query from '../query'; describe('queriesEqual', () => { @@ -79,7 +78,7 @@ describe('parseAsDate', () => { }); describe('serializeDate', () => { - const date = moment.utc('2016-06-20T13:09:48.256Z'); + const date = new Date('2016-06-20T13:09:48.256Z'); it('should serialize string correctly', () => { expect(query.serializeDate(date)).toBe('2016-06-20T13:09:48+0000'); expect(query.serializeDate('')).toBeUndefined(); diff --git a/server/sonar-web/src/main/js/helpers/dates.ts b/server/sonar-web/src/main/js/helpers/dates.ts new file mode 100644 index 00000000000..5bbb50b3bff --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/dates.ts @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ + +const MILLISECONDS_IN_MINUTE = 60 * 1000; +const MILLISECONDS_IN_DAY = MILLISECONDS_IN_MINUTE * 60 * 24; + +function pad(number: number) { + if (number < 10) { + return '0' + number; + } + return number; +} + +function compareDateAsc(dateLeft: Date, dateRight: Date): number { + var timeLeft = dateLeft.getTime(); + var timeRight = dateRight.getTime(); + + if (timeLeft < timeRight) { + return -1; + } else if (timeLeft > timeRight) { + return 1; + } else { + return 0; + } +} + +export function toShortNotSoISOString(date: Date): string { + return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); +} + +export function toNotSoISOString(date: Date): string { + return date.toISOString().replace(/\..+Z$/, '+0000'); +} + +export function startOfDay(date: Date): Date { + const startDay = new Date(date); + startDay.setHours(0, 0, 0, 0); + return startDay; +} + +export function isValidDate(date: Date): boolean { + return !isNaN(date.getTime()); +} + +export function isSameDay(dateLeft: Date, dateRight: Date): boolean { + const startDateLeft = startOfDay(dateLeft); + const startDateRight = startOfDay(dateRight); + return startDateLeft.getTime() === startDateRight.getTime(); +} + +export function differenceInYears(dateLeft: Date, dateRight: Date): number { + const sign = compareDateAsc(dateLeft, dateRight); + const diff = Math.abs(dateLeft.getFullYear() - dateRight.getFullYear()); + const tmpLeftDate = new Date(dateLeft); + tmpLeftDate.setFullYear(dateLeft.getFullYear() - sign * diff); + const isLastYearNotFull = compareDateAsc(tmpLeftDate, dateRight) === -sign; + return sign * (diff - (isLastYearNotFull ? 1 : 0)); +} + +export function differenceInDays(dateLeft: Date, dateRight: Date): number { + const startDateLeft = startOfDay(dateLeft); + const startDateRight = startOfDay(dateRight); + const timestampLeft = + startDateLeft.getTime() - startDateLeft.getTimezoneOffset() * MILLISECONDS_IN_MINUTE; + const timestampRight = + startDateRight.getTime() - startDateRight.getTimezoneOffset() * MILLISECONDS_IN_MINUTE; + return Math.round((timestampLeft - timestampRight) / MILLISECONDS_IN_DAY); +} + +export function differenceInSeconds(dateLeft: Date, dateRight: Date): number { + const diff = (dateLeft.getTime() - dateRight.getTime()) / 1000; + return diff > 0 ? Math.floor(diff) : Math.ceil(diff); +} diff --git a/server/sonar-web/src/main/js/helpers/handlebars/d.js b/server/sonar-web/src/main/js/helpers/handlebars/d.js index d457edd9fdb..ef43101b332 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/d.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/d.js @@ -17,8 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); - module.exports = function(date) { - return moment(date).format('LL'); + return new Intl.DateTimeFormat(localStorage.getItem('l10n.locale') || 'en', { + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(new Date(date)); }; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/dt.js b/server/sonar-web/src/main/js/helpers/handlebars/dt.js index 708be097e33..3af77ae1d6c 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/dt.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/dt.js @@ -17,8 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); - module.exports = function(date) { - return moment(date).format('LLL'); + return new Intl.DateTimeFormat(localStorage.getItem('l10n.locale') || 'en', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric' + }).format(new Date(date)); }; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js b/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js index dc607b8dca2..ea25726d79f 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); +const IntlRelativeFormat = require('intl-relativeformat'); module.exports = function(date) { - return moment(date).fromNow(); + return new IntlRelativeFormat(localStorage.getItem('l10n.locale') || 'en').format(date); }; diff --git a/server/sonar-web/src/main/js/helpers/l10n.js b/server/sonar-web/src/main/js/helpers/l10n.ts index 1f5ebda6796..57f51949074 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.js +++ b/server/sonar-web/src/main/js/helpers/l10n.ts @@ -17,21 +17,36 @@ * 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 moment from 'moment'; import { getJSON } from './request'; +import { toNotSoISOString } from './dates'; -let messages = {}; +interface LanguageBundle { + [name: string]: string; +} + +interface BundleRequestParams { + locale?: string; + ts?: string; +} + +interface BundleRequestResponse { + effectiveLocale: string; + messages: LanguageBundle; +} -export function translate(...keys /*: string[] */) { +let messages: LanguageBundle = {}; + +export const DEFAULT_LANGUAGE = 'en'; + +export function translate(...keys: string[]): string { const messageKey = keys.join('.'); return messages[messageKey] || messageKey; } export function translateWithParameters( - messageKey /*: string */, - ...parameters /*: Array<string | number> */ -) { + messageKey: string, + ...parameters: Array<string | number> +): string { const message = messages[messageKey]; if (message) { return parameters @@ -42,20 +57,16 @@ export function translateWithParameters( } } -export function hasMessage(...keys /*: string[] */) { +export function hasMessage(...keys: string[]): boolean { const messageKey = keys.join('.'); return messages[messageKey] != null; } -export function configureMoment(language /*: ?string */) { - moment.locale(language || getPreferredLanguage()); -} - -function getPreferredLanguage() { +function getPreferredLanguage(): string | undefined { return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; } -function checkCachedBundle() { +function checkCachedBundle(): boolean { const cached = localStorage.getItem('l10n.bundle'); if (!cached) { @@ -70,20 +81,20 @@ function checkCachedBundle() { } } -function getL10nBundle(params) { +function getL10nBundle(params: BundleRequestParams): Promise<BundleRequestResponse> { const url = '/api/l10n/index'; return getJSON(url, params); } -export function requestMessages() { +export function requestMessages(): Promise<string> { const browserLocale = getPreferredLanguage(); const cachedLocale = localStorage.getItem('l10n.locale'); - const params = {}; + const params: BundleRequestParams = {}; if (browserLocale) { params.locale = browserLocale; - if (browserLocale.startsWith(cachedLocale)) { + if (cachedLocale && browserLocale.startsWith(cachedLocale)) { const bundleTimestamp = localStorage.getItem('l10n.timestamp'); if (bundleTimestamp !== null && checkCachedBundle()) { params.ts = bundleTimestamp; @@ -92,52 +103,52 @@ export function requestMessages() { } return getL10nBundle(params).then( - ({ effectiveLocale, messages }) => { + ({ effectiveLocale, messages }: BundleRequestResponse) => { try { - const currentTimestamp = moment().format('YYYY-MM-DDTHH:mm:ssZZ'); + const currentTimestamp = toNotSoISOString(new Date()); localStorage.setItem('l10n.timestamp', currentTimestamp); localStorage.setItem('l10n.locale', effectiveLocale); localStorage.setItem('l10n.bundle', JSON.stringify(messages)); } catch (e) { // do nothing } - configureMoment(effectiveLocale); resetBundle(messages); + return effectiveLocale || browserLocale || DEFAULT_LANGUAGE; }, ({ response }) => { if (response && response.status === 304) { - configureMoment(cachedLocale || browserLocale); resetBundle(JSON.parse(localStorage.getItem('l10n.bundle') || '{}')); } else { throw new Error('Unexpected status code: ' + response.status); } + return cachedLocale || browserLocale || DEFAULT_LANGUAGE; } ); } -export function resetBundle(bundle /*: Object */) { +export function resetBundle(bundle: LanguageBundle) { messages = bundle; } export function installGlobal() { - window.t = translate; - window.tp = translateWithParameters; - window.requestMessages = requestMessages; + (window as any).t = translate; + (window as any).tp = translateWithParameters; + (window as any).requestMessages = requestMessages; } -export function getLocalizedDashboardName(baseName /*: string */) { +export function getLocalizedDashboardName(baseName: string) { const l10nKey = `dashboard.${baseName}.name`; const l10nLabel = translate(l10nKey); return l10nLabel !== l10nKey ? l10nLabel : baseName; } -export function getLocalizedMetricName(metric /*: { key: string, name: string } */) { +export function getLocalizedMetricName(metric: { key: string; name: string }) { const bundleKey = `metric.${metric.key}.name`; const fromBundle = translate(bundleKey); return fromBundle !== bundleKey ? fromBundle : metric.name; } -export function getLocalizedMetricDomain(domainName /*: string */) { +export function getLocalizedMetricDomain(domainName: string) { const bundleKey = `metric_domain.${domainName}`; const fromBundle = translate(bundleKey); return fromBundle !== bundleKey ? fromBundle : domainName; diff --git a/server/sonar-web/src/main/js/helpers/periods.js b/server/sonar-web/src/main/js/helpers/periods.js index 0677d81c13c..4c5ac1c876d 100644 --- a/server/sonar-web/src/main/js/helpers/periods.js +++ b/server/sonar-web/src/main/js/helpers/periods.js @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import { translate, translateWithParameters } from './l10n'; export function getPeriod(periods, index) { @@ -51,7 +50,7 @@ export function getPeriodDate(period) { return null; } - return moment(period.date).toDate(); + return new Date(period.date); } export function getLeakPeriodLabel(periods) { diff --git a/server/sonar-web/src/main/js/helpers/query.js b/server/sonar-web/src/main/js/helpers/query.js index f7c0f2b6a9c..a87eefe5e3f 100644 --- a/server/sonar-web/src/main/js/helpers/query.js +++ b/server/sonar-web/src/main/js/helpers/query.js @@ -19,7 +19,7 @@ */ // @flow import { isNil, omitBy } from 'lodash'; -import moment from 'moment'; +import { isValidDate, toNotSoISOString } from './dates'; /*:: export type RawQuery = { [string]: any }; @@ -65,9 +65,11 @@ export function parseAsBoolean( } export function parseAsDate(value /*: ?string */) /*: Date | void */ { - const date = moment(value); - if (value && date) { - return date.toDate(); + if (value) { + const date = new Date(value); + if (isValidDate(date)) { + return date; + } } } @@ -85,7 +87,7 @@ export function parseAsArray(value /*: ?string */, itemParser /*: string => * */ export function serializeDate(value /*: ?Date */) /*: string | void */ { if (value != null && value.toISOString) { - return moment(value).format('YYYY-MM-DDTHH:mm:ssZZ'); + return toNotSoISOString(value); } } diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index deed3501e74..a0931769355 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ShallowWrapper } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { IntlProvider } from 'react-intl'; export const mockEvent = { target: { blur() {} }, @@ -69,3 +70,9 @@ export function doAsync(fn: Function): Promise<void> { }, 0); }); } + +const intlProvider = new IntlProvider({ locale: 'en' }, {}); +const { intl } = intlProvider.getChildContext(); +export function shallowWithIntl(node, options = {}) { + return shallow(node, { ...options, context: { intl, ...options.context } }); +} diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index b3a14ca74f1..fc6cfa7afab 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -65,6 +65,10 @@ dependencies: "@types/react" "*" +"@types/react-intl@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-2.3.1.tgz#d036dbe54f6ef29f2a150ed303a84f1693ddf905" + "@types/react-modal@2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-2.2.0.tgz#e92bb8454e53030581f263e3fb7e7d27e3eb85b8" @@ -3433,7 +3437,39 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: +intl-format-cache@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.0.5.tgz#b484cefcb9353f374f25de389a3ceea1af18d7c9" + +intl-messageformat-parser@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.2.0.tgz#5906b7f953ab7470e0dc8549097b648b991892ff" + +intl-messageformat@1.3.0, intl-messageformat@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-1.3.0.tgz#f7d926aded7a3ab19b2dc601efd54e99a4bd4eae" + dependencies: + intl-messageformat-parser "1.2.0" + +intl-messageformat@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.1.0.tgz#1c51da76f02a3f7b360654cdc51bbc4d3fa6c72c" + dependencies: + intl-messageformat-parser "1.2.0" + +intl-relativeformat@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.0.0.tgz#d6ba9dc6c625819bc0abdb1d4e238138b7488f26" + dependencies: + intl-messageformat "^2.0.0" + +intl-relativeformat@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-1.3.0.tgz#893dc7076fccd380cf091a2300c380fa57ace45b" + dependencies: + intl-messageformat "1.3.0" + +invariant@^2.0.0, invariant@^2.1.1, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -4550,10 +4586,6 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi dependencies: minimist "0.0.8" -moment@2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -5673,6 +5705,15 @@ react-input-autosize@^1.1.3: create-react-class "^15.5.2" prop-types "^15.5.8" +react-intl@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.3.0.tgz#e1df6af5667fdf01cbe4aab20e137251e2ae5142" + dependencies: + intl-format-cache "^2.0.5" + intl-messageformat "^1.3.0" + intl-relativeformat "^1.3.0" + invariant "^2.1.1" + react-modal@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-2.2.2.tgz#4bbf98bc506e61c446c9f57329c7a488ea7d504b" |