aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/app/components/LocalizationContainer.tsx76
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js15
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js45
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.js)4
-rw-r--r--server/sonar-web/src/main/js/app/utils/exposeLibraries.js2
-rw-r--r--server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js4
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.js18
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/Task.js6
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx (renamed from server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.js)34
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx (renamed from server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.js)24
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/types.ts (renamed from server/sonar-web/src/main/js/apps/background-tasks/types.js)12
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js12
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js6
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap14
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js97
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.js4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.js37
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.js12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/ApplicationLeakPeriodLegend-test.js.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap34
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/Analysis.js4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/Event.js20
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap14
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js28
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/enhance.js14
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap78
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js73
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js10
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js10
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js17
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap9
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap35
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/utils.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.js27
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.js14
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.js8
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.js.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.js.snap8
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/utils.ts12
-rw-r--r--server/sonar-web/src/main/js/apps/settings/licenses/LicenseRow.js4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicenseRow-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/charts/Timeline.js237
-rw-r--r--server/sonar-web/src/main/js/components/controls/DateInput.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/intl/DateFormatter.tsx41
-rw-r--r--server/sonar-web/src/main/js/components/intl/DateTimeFormatter.tsx38
-rw-r--r--server/sonar-web/src/main/js/components/intl/DateTooltipFormatter.tsx (renamed from server/sonar-web/src/main/js/components/ui/FormattedDate.js)51
-rw-r--r--server/sonar-web/src/main/js/components/intl/TimeFormatter.tsx41
-rw-r--r--server/sonar-web/src/main/js/components/intl/TimeTooltipFormatter.tsx (renamed from server/sonar-web/src/main/js/app/components/LocalizationContainer.js)53
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js27
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js4
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js5
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap72
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap21
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js6
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap8
-rw-r--r--server/sonar-web/src/main/js/components/widgets/barchart.js24
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts69
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts (renamed from server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js)0
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/query-test.js3
-rw-r--r--server/sonar-web/src/main/js/helpers/dates.ts90
-rw-r--r--server/sonar-web/src/main/js/helpers/handlebars/d.js8
-rw-r--r--server/sonar-web/src/main/js/helpers/handlebars/dt.js10
-rw-r--r--server/sonar-web/src/main/js/helpers/handlebars/fromNow.js4
-rw-r--r--server/sonar-web/src/main/js/helpers/l10n.ts (renamed from server/sonar-web/src/main/js/helpers/l10n.js)69
-rw-r--r--server/sonar-web/src/main/js/helpers/periods.js3
-rw-r--r--server/sonar-web/src/main/js/helpers/query.js12
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.ts9
88 files changed, 1055 insertions, 873 deletions
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 } });
+}