'react-dom',
'backbone',
'backbone.marionette',
- 'moment',
'handlebars/runtime',
'./src/main/js/libs/third-party/jquery-ui.js',
'./src/main/js/libs/third-party/select2.js',
"escape-html": "1.0.3",
"handlebars": "2.0.0",
"history": "3.3.0",
+ "intl-relativeformat": "2.0.0",
"jquery": "2.2.0",
"keymaster": "1.6.2",
"lodash": "4.17.4",
- "moment": "2.18.1",
"numeral": "1.5.3",
"rc-tooltip": "3.4.7",
"react": "15.6.1",
"react-dom": "15.6.1",
"react-draggable": "2.2.6",
"react-helmet": "5.1.3",
+ "react-intl": "2.3.0",
"react-modal": "2.2.2",
"react-redux": "5.0.5",
"react-router": "3.0.5",
"@types/react": "16.0.2",
"@types/react-dom": "15.5.2",
"@types/react-helmet": "5.0.3",
+ "@types/react-intl": "2.3.1",
"@types/react-modal": "2.2.0",
"@types/react-redux": "5.0.3",
"@types/react-router": "3.0.5",
+++ /dev/null
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import GlobalLoading from './GlobalLoading';
-import { requestMessages } from '../../helpers/l10n';
-
-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 });
- }
- };
-
- render() {
- if (this.state.loading) {
- return <GlobalLoading />;
- }
- return this.props.children;
- }
-}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
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';
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}
* 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) {
? 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>
);
}
+++ /dev/null
-/*
- * 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 React from 'react';
-import { shallow } from 'enzyme';
-import ComponentNavMeta from '../ComponentNavMeta';
-
-it('renders incremental badge', () => {
- check(true);
- check(false);
-
- function check(incremental) {
- expect(
- shallow(
- <ComponentNavMeta component={{ key: 'foo' }} conf={{}} incremental={incremental} />
- ).find('IncrementalBadge')
- ).toHaveLength(incremental ? 1 : 0);
- }
-});
--- /dev/null
+/*
+ * 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 { shallow } from 'enzyme';
+import ComponentNavMeta from '../ComponentNavMeta';
+
+it('renders incremental badge', () => {
+ check(true);
+ check(false);
+
+ function check(incremental: boolean) {
+ expect(
+ shallow(
+ <ComponentNavMeta component={{ key: 'foo' }} conf={{}} incremental={incremental} />
+ ).find('IncrementalBadge')
+ ).toHaveLength(incremental ? 1 : 0);
+ }
+});
* 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';
import Level from '../../components/ui/Level';
const exposeLibraries = () => {
- window.moment = moment;
window.ReactRedux = ReactRedux;
window.ReactRouter = ReactRouter;
window.SonarIcons = icons;
* 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>}
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', () => {
* 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() {
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);
<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}
+++ /dev/null
-/*
- * 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.
- */
-/* @flow */
-import moment from 'moment';
-import React from 'react';
-
-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;
-
- return (
- <td className="thin nowrap text-right">
- {diff > 0 && <span className="text-warning little-spacer-right">{`(+${diff}d)`}</span>}
-
- {date ? moment(date).format(format) : ''}
- </td>
- );
-};
-
-export default TaskDate;
--- /dev/null
+/*
+ * 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 TimeFormatter from '../../../components/intl/TimeFormatter';
+import { differenceInDays, isValidDate } from '../../../helpers/dates';
+
+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 && isValidDate(parsedDate) ? <TimeFormatter date={parsedDate} long={true} /> : ''}
+ </td>
+ );
+}
+++ /dev/null
-/*
- * 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.
- */
-/* @flow */
-import moment from 'moment';
-import React from 'react';
-/*:: import type { Task } from '../types'; */
-
-function isAnotherDay(a, b) {
- return !moment(a).isSame(moment(b), 'day');
-}
-
-const TaskDay = ({ task, prevTask } /*: { task: Task, prevTask: ?Task } */) => {
- const shouldDisplay = !prevTask || isAnotherDay(task.submittedAt, prevTask.submittedAt);
-
- return (
- <td className="thin nowrap text-right">
- {shouldDisplay ? moment(task.submittedAt).format('LL') : ''}
- </td>
- );
-};
-
-export default TaskDay;
--- /dev/null
+/*
+ * 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 DateFormatter from '../../../components/intl/DateFormatter';
+import { isSameDay } from '../../../helpers/dates';
+import { ITask } from '../types';
+
+interface Props {
+ task: ITask;
+ prevTask?: ITask;
+}
+
+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 ? <DateFormatter date={task.submittedAt} long={true} /> : ''}
+ </td>
+ );
+}
+++ /dev/null
-/*
- * 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.
- */
-/*::
-export type Task = {
- incremental: boolean,
- id: string
-};
-*/
--- /dev/null
+/*
+ * 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.
+ */
+
+export interface ITask {
+ incremental: boolean;
+ id: string;
+ submittedAt: string;
+}
// @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';
}
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}
// @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';
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;
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();
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
*/
// @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'; */
|};
*/
-const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ';
-
export default class CreationDateFacet extends React.PureComponent {
/*:: props: Props; */
open: true
};
+ static contextTypes = {
+ intl: intlShape
+ };
+
property = 'createdAt';
hasValue = () =>
};
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;
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]
};
});
}
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>
);
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">
: <div>
{this.renderBarChart()}
{this.renderPeriodSelectors()}
- {this.renderPrefefinedPeriods()}
+ {this.renderPredefinedPeriods()}
</div>;
}
// @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';
? <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>
*/
// @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';
}
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>
);
// @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';
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;
mode: 'date',
parameter: '2013-01-01'
};
- expect(shallow(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot();
+ expect(
+ shallow(<LeakPeriodLegend period={period} />).find('FormattedRelative')
+ ).toMatchSnapshot();
});
it('version', () => {
mode: 'version',
parameter: '0.1'
};
- expect(shallow(<LeakPeriodLegend period={period} />).find('.note')).toMatchSnapshot();
+ expect(
+ shallow(<LeakPeriodLegend period={period} />).find('FormattedRelative')
+ ).toMatchSnapshot();
});
it('previous_version', () => {
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', () => {
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);
});
});
<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>
`;
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}
+/>
`;
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'; */
<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>
// @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;
}
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>
);
}
*/
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'; */
<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>
className="small little-spacer-bottom"
>
<strong>
- <FormattedDate
+ <DateTooltipFormatter
date="2017-06-10T16:10:59+0200"
- format="LL"
+ placement="right"
/>
</strong>
</div>
`;
exports[`should render an event correctly 1`] = `
-<div
+<span
className="overview-analysis-event"
>
<span
:
</span>
- <Tooltip
- placement="left"
- >
- <strong>
- test
- </strong>
- </Tooltip>
-</div>
+ <strong>
+ test
+ </strong>
+</span>
`;
<div
className="overview-analysis-graph-tooltip-title"
>
- <FormattedDate
+ <DateFormatter
date={2011-10-25T10:27:41.000Z}
- format="LL"
+ long={true}
/>
</div>
<table
* 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';
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)}>
}
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>
);
}
*/
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';
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)}>
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,
},
],
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,
},
],
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,
},
],
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",
},
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",
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",
},
},
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",
"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",
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",
"key": "AVyMjlK1HjR_PLDzRbB9",
},
Object {
- "date": 2017-06-09T09:12:27.000Z,
+ "date": 2017-06-09T11:12:27.000Z,
"events": Array [],
"key": "AVyM9n3cHjR_PLDzRciT",
},
},
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",
"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",
},
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",
"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",
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",
},
*/
// @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',
}
]
},
- { 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' },
{
}
]
},
- { 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' }
]
}
];
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,
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();
});
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, {
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();
});
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: '',
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'
})
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'
});
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'
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 }
]
}
])
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 }
]
}
])
{
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();
* 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';
return acc.concat({
className: event.category,
name: event.name,
- date: moment(analysis.date).toDate()
+ date: new Date(analysis.date)
});
}, []);
return sortBy(filteredEvents, 'date');
// @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';
<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>
// @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,
</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 &&
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'; */
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} />
// @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';
<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}
*/
// @flow
import React from 'react';
-import moment from 'moment';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import ProjectActivityApp from './ProjectActivityApp';
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
}));
};
measures.map(measure => ({
metric: measure.metric,
history: measure.history.map(analysis => ({
- date: moment(analysis.date).toDate(),
+ date: new Date(analysis.date),
value: analysis.value
}))
})),
*/
// @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'; */
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 (
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 = [
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();
});
* 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')}
<div
className="project-activity-graph-tooltip-title spacer-bottom"
>
- <FormattedDate
+ <DateTimeFormatter
date={2011-10-01T22:01:00.000Z}
- format="LL"
/>
</div>
<table
<div
className="project-activity-graph-tooltip-title spacer-bottom"
>
- <FormattedDate
+ <DateTimeFormatter
date={2011-10-01T22:01:00.000Z}
- format="LL"
/>
</div>
<table
<div
className="project-activity-graph-tooltip-title spacer-bottom"
>
- <FormattedDate
+ <DateTimeFormatter
date={2011-10-25T10:27:41.000Z}
- format="LL"
/>
</div>
<table
>
<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
>
<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
>
<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
>
<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
</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
name="from"
onChange={[Function]}
placeholder="from"
- value="2016-10-27"
+ value="10/27/2016"
/>
—
<DateInput
name="to"
onChange={[Function]}
placeholder="to"
- value="2016-12-27"
+ value="12/27/2016"
/>
<button
className="spacer-left"
* 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,
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'; */
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) {
// @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';
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>
// @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';
</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>
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', () => {
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} />)
<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
<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
*/
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 {
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 &&
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">
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', () => {
* 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 {
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>;
*/
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) => {
}
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;
loadLatestRules() {
const data = {
- available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD'),
+ available_since: this.periodStartDate,
s: 'createdAt',
asc: false,
ps: RULES_LIMIT,
}
const newRulesUrl = getRulesUrl(
- {
- available_since: PERIOD_START_MOMENT.format('YYYY-MM-DD')
- },
+ { available_since: this.periodStartDate },
this.props.organization
);
* 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';
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);
{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>
* 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[]) {
};
}
-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) =>
*/
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';
<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">
'.js-expiration'
);
expect(licenseExpiration.length).toBe(1);
- expect(licenseExpiration.text()).toContain('2015');
+ expect(licenseExpiration.find('DateFormatter')).toHaveLength(1);
});
it('should render invalid expiration', () => {
+++ /dev/null
-/*
- * 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;
}
componentWillReceiveProps(nextProps: Props) {
- if (nextProps.value != null) {
+ if (nextProps.value != null && this.input) {
this.input.value = nextProps.value;
}
}
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);
}
}
--- /dev/null
+/*
+ * 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)}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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} />;
+}
--- /dev/null
+/*
+ * 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 DateFormatter from './DateFormatter';
+import DateTimeFormatter from './DateTimeFormatter';
+import Tooltip from '../controls/Tooltip';
+
+interface Props {
+ className?: string;
+ date: Date | string | number;
+ placement?: string;
+}
+
+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>
+ );
+}
--- /dev/null
+/*
+ * 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)}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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 TimeFormatter from './TimeFormatter';
+import Tooltip from '../controls/Tooltip';
+
+interface Props {
+ className?: string;
+ date: Date | string | number;
+ placement?: string;
+}
+
+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>
+ );
+}
*/
// @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'; */
/*::
};
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>
);
}
*/
// @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';
tabIndex={0}
/>
<div className="issue-comment-age">
- ({moment(comment.createdAt).fromNow()})
+ <FormattedRelative value={comment.createdAt} />
</div>
<div className="issue-comment-actions">
{comment.updatable &&
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
updatable: true
};
-jest.mock('moment', () => () => ({ fromNow: () => 'a month ago' }));
-
it('should render correctly a comment that is not updatable', () => {
const element = shallow(
<IssueCommentLine
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>
`;
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>
`;
<div
className="issue-comment-age"
>
- (
- a month ago
- )
+ <FormattedRelative
+ updateInterval={10000}
+ value="2017-03-01T09:36:01+0100"
+ />
</div>
<div
className="issue-comment-actions"
<div
className="issue-comment-age"
>
- (
- a month ago
- )
+ <FormattedRelative
+ updateInterval={10000}
+ value="2017-03-01T09:36:01+0100"
+ />
</div>
<div
className="issue-comment-actions"
<div
className="issue-comment-age"
>
- (
- a month ago
- )
+ <FormattedRelative
+ updateInterval={10000}
+ value="2017-03-01T09:36:01+0100"
+ />
</div>
<div
className="issue-comment-actions"
*/
// @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'; */
<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')}
{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 &&
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
<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"
<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"
+++ /dev/null
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import moment from 'moment';
-
-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;
-
- return (
- <time className={className} dateTime={m.format()} title={title}>
- {m.format(format)}
- </time>
- );
- }
-}
* 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,
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);
.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');
--- /dev/null
+/*
+ * 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);
+});
+++ /dev/null
-/*
- * 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 { resetBundle, translate, translateWithParameters } from '../l10n';
-
-afterEach(() => {
- resetBundle({});
-});
-
-describe('#translate', () => {
- it('should translate simple message', () => {
- resetBundle({ my_key: 'my message' });
- expect(translate('my_key')).toBe('my message');
- });
-
- it('should translate message with composite key', () => {
- resetBundle({ 'my.composite.message': 'my message' });
- expect(translate('my', 'composite', 'message')).toBe('my message');
- expect(translate('my.composite', 'message')).toBe('my message');
- expect(translate('my', 'composite.message')).toBe('my message');
- expect(translate('my.composite.message')).toBe('my message');
- });
-
- it('should not translate message but return its key', () => {
- expect(translate('random')).toBe('random');
- expect(translate('random', 'key')).toBe('random.key');
- expect(translate('composite.random', 'key')).toBe('composite.random.key');
- });
-});
-
-describe('#translateWithParameters', () => {
- it('should translate message with one parameter in the beginning', () => {
- resetBundle({ x_apples: '{0} apples' });
- expect(translateWithParameters('x_apples', 5)).toBe('5 apples');
- });
-
- it('should translate message with one parameter in the middle', () => {
- resetBundle({ x_apples: 'I have {0} apples' });
- expect(translateWithParameters('x_apples', 5)).toBe('I have 5 apples');
- });
-
- it('should translate message with one parameter in the end', () => {
- resetBundle({ x_apples: 'Apples: {0}' });
- expect(translateWithParameters('x_apples', 5)).toBe('Apples: 5');
- });
-
- it('should translate message with several parameters', () => {
- resetBundle({ x_apples: '{0}: I have {2} apples in my {1} baskets - {3}' });
- expect(translateWithParameters('x_apples', 1, 2, 3, 4)).toBe(
- '1: I have 3 apples in my 2 baskets - 4'
- );
- });
-
- it('should not translate message but return its key', () => {
- expect(translateWithParameters('random', 5)).toBe('random.5');
- expect(translateWithParameters('random', 1, 2, 3)).toBe('random.1.2.3');
- expect(translateWithParameters('composite.random', 1, 2)).toBe('composite.random.1.2');
- });
-});
--- /dev/null
+/*
+ * 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 { resetBundle, translate, translateWithParameters } from '../l10n';
+
+afterEach(() => {
+ resetBundle({});
+});
+
+describe('#translate', () => {
+ it('should translate simple message', () => {
+ resetBundle({ my_key: 'my message' });
+ expect(translate('my_key')).toBe('my message');
+ });
+
+ it('should translate message with composite key', () => {
+ resetBundle({ 'my.composite.message': 'my message' });
+ expect(translate('my', 'composite', 'message')).toBe('my message');
+ expect(translate('my.composite', 'message')).toBe('my message');
+ expect(translate('my', 'composite.message')).toBe('my message');
+ expect(translate('my.composite.message')).toBe('my message');
+ });
+
+ it('should not translate message but return its key', () => {
+ expect(translate('random')).toBe('random');
+ expect(translate('random', 'key')).toBe('random.key');
+ expect(translate('composite.random', 'key')).toBe('composite.random.key');
+ });
+});
+
+describe('#translateWithParameters', () => {
+ it('should translate message with one parameter in the beginning', () => {
+ resetBundle({ x_apples: '{0} apples' });
+ expect(translateWithParameters('x_apples', 5)).toBe('5 apples');
+ });
+
+ it('should translate message with one parameter in the middle', () => {
+ resetBundle({ x_apples: 'I have {0} apples' });
+ expect(translateWithParameters('x_apples', 5)).toBe('I have 5 apples');
+ });
+
+ it('should translate message with one parameter in the end', () => {
+ resetBundle({ x_apples: 'Apples: {0}' });
+ expect(translateWithParameters('x_apples', 5)).toBe('Apples: 5');
+ });
+
+ it('should translate message with several parameters', () => {
+ resetBundle({ x_apples: '{0}: I have {2} apples in my {1} baskets - {3}' });
+ expect(translateWithParameters('x_apples', 1, 2, 3, 4)).toBe(
+ '1: I have 3 apples in my 2 baskets - 4'
+ );
+ });
+
+ it('should not translate message but return its key', () => {
+ expect(translateWithParameters('random', 5)).toBe('random.5');
+ expect(translateWithParameters('random', 1, 2, 3)).toBe('random.1.2.3');
+ expect(translateWithParameters('composite.random', 1, 2)).toBe('composite.random.1.2');
+ });
+});
* 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', () => {
});
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();
--- /dev/null
+/*
+ * 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);
+}
* 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));
};
* 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));
};
* 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);
};
+++ /dev/null
-/*
- * 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.
- */
-/* @flow */
-import moment from 'moment';
-import { getJSON } from './request';
-
-let messages = {};
-
-export function translate(...keys /*: string[] */) {
- const messageKey = keys.join('.');
- return messages[messageKey] || messageKey;
-}
-
-export function translateWithParameters(
- messageKey /*: string */,
- ...parameters /*: Array<string | number> */
-) {
- const message = messages[messageKey];
- if (message) {
- return parameters
- .map(parameter => String(parameter))
- .reduce((acc, parameter, index) => acc.replace(`{${index}}`, parameter), message);
- } else {
- return `${messageKey}.${parameters.join('.')}`;
- }
-}
-
-export function hasMessage(...keys /*: string[] */) {
- const messageKey = keys.join('.');
- return messages[messageKey] != null;
-}
-
-export function configureMoment(language /*: ?string */) {
- moment.locale(language || getPreferredLanguage());
-}
-
-function getPreferredLanguage() {
- return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language;
-}
-
-function checkCachedBundle() {
- const cached = localStorage.getItem('l10n.bundle');
-
- if (!cached) {
- return false;
- }
-
- try {
- const parsed = JSON.parse(cached);
- return parsed != null && typeof parsed === 'object';
- } catch (e) {
- return false;
- }
-}
-
-function getL10nBundle(params) {
- const url = '/api/l10n/index';
- return getJSON(url, params);
-}
-
-export function requestMessages() {
- const browserLocale = getPreferredLanguage();
- const cachedLocale = localStorage.getItem('l10n.locale');
- const params = {};
-
- if (browserLocale) {
- params.locale = browserLocale;
-
- if (browserLocale.startsWith(cachedLocale)) {
- const bundleTimestamp = localStorage.getItem('l10n.timestamp');
- if (bundleTimestamp !== null && checkCachedBundle()) {
- params.ts = bundleTimestamp;
- }
- }
- }
-
- return getL10nBundle(params).then(
- ({ effectiveLocale, messages }) => {
- try {
- const currentTimestamp = moment().format('YYYY-MM-DDTHH:mm:ssZZ');
- 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);
- },
- ({ 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);
- }
- }
- );
-}
-
-export function resetBundle(bundle /*: Object */) {
- messages = bundle;
-}
-
-export function installGlobal() {
- window.t = translate;
- window.tp = translateWithParameters;
- window.requestMessages = requestMessages;
-}
-
-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 } */) {
- const bundleKey = `metric.${metric.key}.name`;
- const fromBundle = translate(bundleKey);
- return fromBundle !== bundleKey ? fromBundle : metric.name;
-}
-
-export function getLocalizedMetricDomain(domainName /*: string */) {
- const bundleKey = `metric_domain.${domainName}`;
- const fromBundle = translate(bundleKey);
- return fromBundle !== bundleKey ? fromBundle : domainName;
-}
--- /dev/null
+/*
+ * 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 { getJSON } from './request';
+import { toNotSoISOString } from './dates';
+
+interface LanguageBundle {
+ [name: string]: string;
+}
+
+interface BundleRequestParams {
+ locale?: string;
+ ts?: string;
+}
+
+interface BundleRequestResponse {
+ effectiveLocale: string;
+ messages: LanguageBundle;
+}
+
+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>
+): string {
+ const message = messages[messageKey];
+ if (message) {
+ return parameters
+ .map(parameter => String(parameter))
+ .reduce((acc, parameter, index) => acc.replace(`{${index}}`, parameter), message);
+ } else {
+ return `${messageKey}.${parameters.join('.')}`;
+ }
+}
+
+export function hasMessage(...keys: string[]): boolean {
+ const messageKey = keys.join('.');
+ return messages[messageKey] != null;
+}
+
+function getPreferredLanguage(): string | undefined {
+ return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language;
+}
+
+function checkCachedBundle(): boolean {
+ const cached = localStorage.getItem('l10n.bundle');
+
+ if (!cached) {
+ return false;
+ }
+
+ try {
+ const parsed = JSON.parse(cached);
+ return parsed != null && typeof parsed === 'object';
+ } catch (e) {
+ return false;
+ }
+}
+
+function getL10nBundle(params: BundleRequestParams): Promise<BundleRequestResponse> {
+ const url = '/api/l10n/index';
+ return getJSON(url, params);
+}
+
+export function requestMessages(): Promise<string> {
+ const browserLocale = getPreferredLanguage();
+ const cachedLocale = localStorage.getItem('l10n.locale');
+ const params: BundleRequestParams = {};
+
+ if (browserLocale) {
+ params.locale = browserLocale;
+
+ if (cachedLocale && browserLocale.startsWith(cachedLocale)) {
+ const bundleTimestamp = localStorage.getItem('l10n.timestamp');
+ if (bundleTimestamp !== null && checkCachedBundle()) {
+ params.ts = bundleTimestamp;
+ }
+ }
+ }
+
+ return getL10nBundle(params).then(
+ ({ effectiveLocale, messages }: BundleRequestResponse) => {
+ try {
+ 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
+ }
+ resetBundle(messages);
+ return effectiveLocale || browserLocale || DEFAULT_LANGUAGE;
+ },
+ ({ response }) => {
+ if (response && response.status === 304) {
+ 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: LanguageBundle) {
+ messages = bundle;
+}
+
+export function installGlobal() {
+ (window as any).t = translate;
+ (window as any).tp = translateWithParameters;
+ (window as any).requestMessages = requestMessages;
+}
+
+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 }) {
+ const bundleKey = `metric.${metric.key}.name`;
+ const fromBundle = translate(bundleKey);
+ return fromBundle !== bundleKey ? fromBundle : metric.name;
+}
+
+export function getLocalizedMetricDomain(domainName: string) {
+ const bundleKey = `metric_domain.${domainName}`;
+ const fromBundle = translate(bundleKey);
+ return fromBundle !== bundleKey ? fromBundle : domainName;
+}
* 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) {
return null;
}
- return moment(period.date).toDate();
+ return new Date(period.date);
}
export function getLeakPeriodLabel(periods) {
*/
// @flow
import { isNil, omitBy } from 'lodash';
-import moment from 'moment';
+import { isValidDate, toNotSoISOString } from './dates';
/*::
export type RawQuery = { [string]: any };
}
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;
+ }
}
}
export function serializeDate(value /*: ?Date */) /*: string | void */ {
if (value != null && value.toISOString) {
- return moment(value).format('YYYY-MM-DDTHH:mm:ssZZ');
+ return toNotSoISOString(value);
}
}
* 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() {} },
}, 0);
});
}
+
+const intlProvider = new IntlProvider({ locale: 'en' }, {});
+const { intl } = intlProvider.getChildContext();
+export function shallowWithIntl(node, options = {}) {
+ return shallow(node, { ...options, context: { intl, ...options.context } });
+}
dependencies:
"@types/react" "*"
+"@types/react-intl@2.3.1":
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-2.3.1.tgz#d036dbe54f6ef29f2a150ed303a84f1693ddf905"
+
"@types/react-modal@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-2.2.0.tgz#e92bb8454e53030581f263e3fb7e7d27e3eb85b8"
version "1.0.3"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90"
-invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
+intl-format-cache@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.0.5.tgz#b484cefcb9353f374f25de389a3ceea1af18d7c9"
+
+intl-messageformat-parser@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.2.0.tgz#5906b7f953ab7470e0dc8549097b648b991892ff"
+
+intl-messageformat@1.3.0, intl-messageformat@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-1.3.0.tgz#f7d926aded7a3ab19b2dc601efd54e99a4bd4eae"
+ dependencies:
+ intl-messageformat-parser "1.2.0"
+
+intl-messageformat@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.1.0.tgz#1c51da76f02a3f7b360654cdc51bbc4d3fa6c72c"
+ dependencies:
+ intl-messageformat-parser "1.2.0"
+
+intl-relativeformat@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.0.0.tgz#d6ba9dc6c625819bc0abdb1d4e238138b7488f26"
+ dependencies:
+ intl-messageformat "^2.0.0"
+
+intl-relativeformat@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-1.3.0.tgz#893dc7076fccd380cf091a2300c380fa57ace45b"
+ dependencies:
+ intl-messageformat "1.3.0"
+
+invariant@^2.0.0, invariant@^2.1.1, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
dependencies:
dependencies:
minimist "0.0.8"
-moment@2.18.1:
- version "2.18.1"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
-
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
create-react-class "^15.5.2"
prop-types "^15.5.8"
+react-intl@2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.3.0.tgz#e1df6af5667fdf01cbe4aab20e137251e2ae5142"
+ dependencies:
+ intl-format-cache "^2.0.5"
+ intl-messageformat "^1.3.0"
+ intl-relativeformat "^1.3.0"
+ invariant "^2.1.1"
+
react-modal@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-2.2.2.tgz#4bbf98bc506e61c446c9f57329c7a488ea7d504b"
quality_profiles.list.updated=Updated
quality_profiles.list.used=Used
quality_profiles.x_activated_out_of_y={0} rules activated out of {1} available
+quality_profiles.x_updated_on_y={0}, updated on {1}
quality_profiles.change_projects=Change Projects
quality_profiles.not_found=The requested quality profile was not found.
quality_profiles.latest_new_rules=Recently Added Rules