refactor(moment): Replace Moment.js with DayJs (#31310)

This commit is contained in:
Mehmet Salih Yavuz 2024-12-23 20:42:43 +03:00 committed by GitHub
parent b382ef1058
commit a193d790b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 268 additions and 212 deletions

View File

@ -34175,7 +34175,8 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/lodash.escape": {
"version": "4.0.1",
@ -39964,6 +39965,8 @@
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
@ -39974,6 +39977,7 @@
"resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz",
"integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.difference": "^4.5.0"
},
@ -58121,9 +58125,9 @@
"license": "Apache-2.0",
"dependencies": {
"d3-array": "^1.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.4.1",
"lodash": "^4.17.21",
"moment": "^2.30.1"
"lodash": "^4.17.21"
},
"peerDependencies": {
"@superset-ui/chart-controls": "*",
@ -58150,8 +58154,8 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"ace-builds": "^1.4.14",
"dayjs": "^1.11.13",
"lodash": "^4.17.11",
"moment": "^2.26.0",
"react": "^16.13.1",
"react-ace": "^10.1.0",
"react-dom": "^16.13.1"
@ -68727,9 +68731,9 @@
"version": "file:plugins/plugin-chart-echarts",
"requires": {
"d3-array": "^1.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.4.1",
"lodash": "^4.17.21",
"moment": "^2.30.1"
"lodash": "^4.17.21"
}
},
"@superset-ui/plugin-chart-handlebars": {
@ -86638,7 +86642,9 @@
"dev": true
},
"moment": {
"version": "2.30.1"
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
},
"moment-locales-webpack-plugin": {
"version": "1.2.0",

View File

@ -36,7 +36,6 @@
"d3-color": "^1.4.1",
"d3-scale": "^3.0.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"mousetrap": "^1.6.5",
"prop-types": "^15.8.1",
"react-bootstrap-slider": "3.0.0",

View File

@ -35,7 +35,7 @@
"dompurify": "^3.2.3",
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"dayjs": "^1.11.13",
"nvd3-fork": "^2.0.5",
"prop-types": "^15.8.1",
"urijs": "^1.19.11"

View File

@ -19,7 +19,8 @@
*/
import { kebabCase, throttle } from 'lodash';
import d3 from 'd3';
import moment from 'moment';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import nv from 'nvd3-fork';
import PropTypes from 'prop-types';
import {
@ -83,6 +84,8 @@ const NO_DATA_RENDER_DATA = [
},
];
dayjs.extend(utc);
const smartDateVerboseFormatter = getTimeFormatter(SMART_DATE_VERBOSE_ID);
// Override the noData render function to make a prettier UX
@ -1055,7 +1058,7 @@ function nvd3Vis(element, props) {
});
const records = (annotationData[e.name].records || [])
.map(r => {
const timeValue = new Date(moment.utc(r[e.timeColumn]));
const timeValue = new Date(dayjs.utc(r[e.timeColumn]));
return {
...r,
@ -1131,9 +1134,9 @@ function nvd3Vis(element, props) {
const records = (annotationData[e.name].records || [])
.map(r => {
const timeValue = new Date(moment.utc(r[e.timeColumn]));
const timeValue = new Date(dayjs.utc(r[e.timeColumn]));
const intervalEndValue = new Date(
moment.utc(r[e.intervalEndColumn]),
dayjs.utc(r[e.intervalEndColumn]),
);
return {

View File

@ -27,7 +27,7 @@
"d3-array": "^1.2.0",
"echarts": "^5.4.1",
"lodash": "^4.17.21",
"moment": "^2.30.1"
"dayjs": "^1.11.13"
},
"peerDependencies": {
"@superset-ui/chart-controls": "*",

View File

@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import moment from 'moment';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import {
ChartProps,
getMetricLabel,
@ -27,9 +28,11 @@ import {
} from '@superset-ui/core';
import { getComparisonFontSize, getHeaderFontSize } from './utils';
dayjs.extend(utc);
export const parseMetricValue = (metricValue: number | string | null) => {
if (typeof metricValue === 'string') {
const dateObject = moment.utc(metricValue, moment.ISO_8601, true);
const dateObject = dayjs.utc(metricValue, undefined, true);
if (dateObject.isValid()) {
return dateObject.valueOf();
}

View File

@ -17,7 +17,8 @@
* under the License.
*/
import moment from 'moment';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import {
getTimeFormatter,
getTimeFormatterForGranularity,
@ -25,9 +26,11 @@ import {
TimeGranularity,
} from '@superset-ui/core';
dayjs.extend(utc);
export const parseMetricValue = (metricValue: number | string | null) => {
if (typeof metricValue === 'string') {
const dateObject = moment.utc(metricValue, moment.ISO_8601, true);
const dateObject = dayjs.utc(metricValue, undefined, true);
if (dateObject.isValid()) {
return dateObject.valueOf();
}

View File

@ -36,7 +36,7 @@
"@superset-ui/core": "*",
"ace-builds": "^1.4.14",
"lodash": "^4.17.11",
"moment": "^2.26.0",
"dayjs": "^1.11.13",
"react": "^16.13.1",
"react-ace": "^10.1.0",
"react-dom": "^16.13.1"

View File

@ -18,7 +18,7 @@
*/
import { SafeMarkdown, styled, t } from '@superset-ui/core';
import Handlebars from 'handlebars';
import moment from 'moment';
import dayjs from 'dayjs';
import { useMemo, useState } from 'react';
import { isPlainObject } from 'lodash';
import Helpers from 'just-handlebars-helpers';
@ -78,7 +78,7 @@ export const HandlebarsViewer = ({
// usage: {{dateFormat my_date format="MMMM YYYY"}}
Handlebars.registerHelper('dateFormat', function (context, block) {
const f = block.hash.format || 'YYYY-MM-DD';
return moment(context).format(f);
return dayjs(context).format(f);
});
// usage: {{ }}

View File

@ -17,7 +17,6 @@
* under the License.
*/
import { useMemo, ReactNode } from 'react';
import moment from 'moment';
import Card from 'src/components/Card';
import ProgressBar from 'src/components/ProgressBar';
import { t, useTheme, QueryResponse } from '@superset-ui/core';
@ -32,7 +31,7 @@ import {
} from 'src/SqlLab/actions/sqlLab';
import TableView from 'src/components/TableView';
import Button from 'src/components/Button';
import { fDuration } from 'src/utils/dates';
import { fDuration, extendedDayjs } from 'src/utils/dates';
import Icons from 'src/components/Icons';
import Label from 'src/components/Label';
import { Tooltip } from 'src/components/Tooltip';
@ -255,7 +254,9 @@ const QueryTable = ({
</Button>
);
q.started = (
<Label monospace>{moment(q.startDttm).format('L HH:mm:ss')}</Label>
<Label monospace>
{extendedDayjs(q.startDttm).format('L HH:mm:ss')}
</Label>
);
q.querylink = (
<Button

View File

@ -35,7 +35,7 @@ import {
VizType,
} from '@superset-ui/core';
import { useSelector, useDispatch } from 'react-redux';
import moment from 'moment';
import dayjs from 'dayjs';
import rison from 'rison';
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
import { addDangerToast } from 'src/components/MessageToasts/actions';
@ -164,7 +164,7 @@ export const SaveDatasetModal = ({
);
const getDefaultDatasetName = () =>
`${datasource?.name || UNTITLED} ${moment().format('L HH:mm:ss')}`;
`${datasource?.name || UNTITLED} ${dayjs().format('L HH:mm:ss')}`;
const [datasetName, setDatasetName] = useState(getDefaultDatasetName());
const [newOrOverwrite, setNewOrOverwrite] = useState(
DatasetRadioState.SaveNew,

View File

@ -18,7 +18,7 @@
*/
import { render, screen } from 'spec/helpers/testing-library';
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import { TooltipContent } from './TooltipContent';
test('Rendering TooltipContent correctly - no timestep', () => {
@ -31,7 +31,7 @@ test('Rendering TooltipContent correctly - no timestep', () => {
test('Rendering TooltipContent correctly - with timestep', () => {
render(<TooltipContent cachedTimestamp="01-01-2000" />);
expect(screen.getByTestId('tooltip-content')?.textContent).toBe(
`Loaded data cached ${moment
`Loaded data cached ${extendedDayjs
.utc('01-01-2000')
.fromNow()}. Click to force-refresh`,
);

View File

@ -18,7 +18,7 @@
*/
import { FC } from 'react';
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import { t } from '@superset-ui/core';
interface Props {
@ -28,7 +28,7 @@ export const TooltipContent: FC<Props> = ({ cachedTimestamp }) => {
const cachedText = cachedTimestamp ? (
<span>
{t('Loaded data cached')}
<b> {moment.utc(cachedTimestamp).fromNow()}</b>
<b> {extendedDayjs.utc(cachedTimestamp).fromNow()}</b>
</span>
) : (
t('Loaded from cache')

View File

@ -18,7 +18,6 @@
*/
/* eslint no-undef: 'error' */
/* eslint no-param-reassign: ["error", { "props": false }] */
import moment from 'moment';
import {
FeatureFlag,
isDefined,
@ -43,6 +42,7 @@ import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesCo
import { updateDataMask } from 'src/dataMask/actions';
import { waitForAsyncData } from 'src/middleware/asyncEvent';
import { safeStringify } from 'src/utils/safeStringify';
import { extendedDayjs } from 'src/utils/dates';
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
export function chartUpdateStarted(queryController, latestQueryFormData, key) {
@ -454,7 +454,9 @@ export function exploreJSON(
formData.extra_filters && formData.extra_filters.length > 0,
viz_type: formData.viz_type,
data_age: resultItem.is_cached
? moment(new Date()).diff(moment.utc(resultItem.cached_dttm))
? extendedDayjs(new Date()).diff(
extendedDayjs.utc(resultItem.cached_dttm),
)
: null,
}),
),

View File

@ -23,17 +23,18 @@ import {
MouseEventHandler,
} from 'react';
import moment, { Moment, MomentInput } from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import { t, styled } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import dayjs from 'dayjs';
const REFRESH_INTERVAL = 60000; // every minute
interface LastUpdatedProps {
updatedAt: MomentInput;
updatedAt: string | number | Date | undefined;
update?: MouseEventHandler<HTMLSpanElement>;
}
moment.updateLocale('en', {
extendedDayjs.updateLocale('en', {
calendar: {
lastDay: '[Yesterday at] LTS',
sameDay: '[Today at] LTS',
@ -62,14 +63,16 @@ export const LastUpdated: FunctionComponent<LastUpdatedProps> = ({
updatedAt,
update,
}) => {
const [timeSince, setTimeSince] = useState<Moment>(moment(updatedAt));
const [timeSince, setTimeSince] = useState<dayjs.Dayjs>(
extendedDayjs(updatedAt),
);
useEffect(() => {
setTimeSince(() => moment(updatedAt));
setTimeSince(() => extendedDayjs(updatedAt));
// update UI every minute in case day changes
const interval = setInterval(() => {
setTimeSince(() => moment(updatedAt));
setTimeSince(() => extendedDayjs(updatedAt));
}, REFRESH_INTERVAL);
return () => clearInterval(interval);

View File

@ -24,6 +24,7 @@ import {
RefObject,
} from 'react';
// TODO: @msyavuz - Replace with dayjs after migrating datepicker to antd5
import moment, { Moment } from 'moment';
import { styled, t } from '@superset-ui/core';
import { RangePicker } from 'src/components/DatePicker';

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import Timer, { TimerProps } from './index';
export default {
@ -32,7 +32,7 @@ InteractiveTimer.args = {
InteractiveTimer.argTypes = {
startTime: {
defaultValue: moment().utc().valueOf(),
defaultValue: extendedDayjs().utc().valueOf(),
table: {
disable: true,
},

View File

@ -18,12 +18,7 @@
*/
import { FC } from 'react';
import {
render,
waitFor,
screen,
waitForElementToBeRemoved,
} from 'spec/helpers/testing-library';
import { render, waitFor, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import type { TimezoneSelectorProps } from './index';
@ -52,7 +47,6 @@ test('render timezones in correct order for daylight saving time', async () => {
/>,
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
const searchInput = screen.getByRole('combobox');
userEvent.click(searchInput);
@ -62,7 +56,7 @@ test('render timezones in correct order for daylight saving time', async () => {
// first option is always current timezone
expect(options[0]).toHaveTextContent('GMT -04:00 (Eastern Daylight Time)');
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)');
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Midway)');
expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)');
expect(options[3]).toHaveTextContent('GMT -09:30 (Pacific/Marquesas)');
});

View File

@ -17,14 +17,9 @@
* under the License.
*/
import { FC } from 'react';
import moment from 'moment-timezone';
import { extendedDayjs } from 'src/utils/dates';
import userEvent from '@testing-library/user-event';
import {
render,
screen,
waitFor,
waitForElementToBeRemoved,
} from 'spec/helpers/testing-library';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import type { TimezoneSelectorProps } from './index';
const loadComponent = (mockCurrentTime?: string) => {
@ -46,20 +41,18 @@ const openSelectMenu = () => {
userEvent.click(searchInput);
};
jest.spyOn(moment.tz, 'guess').mockReturnValue('America/New_York');
jest.spyOn(extendedDayjs.tz, 'guess').mockReturnValue('America/New_York');
afterEach(() => {
jest.useRealTimers();
});
test('use the timezone from `moment` if no timezone provided', async () => {
test('use the timezone from `dayjs` if no timezone provided', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = jest.fn();
render(<TimezoneSelector onTimezoneChange={onTimezoneChange} />);
expect(screen.getByLabelText('Loading')).toBeVisible();
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenCalledWith('America/Nassau');
expect(onTimezoneChange).toHaveBeenCalledWith('America/Detroit');
});
test('update to closest deduped timezone when timezone is provided', async () => {
@ -68,12 +61,11 @@ test('update to closest deduped timezone when timezone is provided', async () =>
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="America/Los_Angeles"
timezone="America/Tijuana"
/>,
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Vancouver');
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Los_Angeles');
});
test('use the default timezone when an invalid timezone is provided', async () => {
@ -82,7 +74,6 @@ test('use the default timezone when an invalid timezone is provided', async () =
render(
<TimezoneSelector onTimezoneChange={onTimezoneChange} timezone="UTC" />,
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('Africa/Abidjan');
});
@ -96,12 +87,11 @@ test('render timezones in correct order for standard time', async () => {
timezone="America/Nassau"
/>,
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
openSelectMenu();
const options = await getSelectOptions();
expect(options[0]).toHaveTextContent('GMT -05:00 (Eastern Standard Time)');
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)');
expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)');
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Midway)');
expect(options[2]).toHaveTextContent('GMT -10:00 (America/Adak)');
});
test('can select a timezone values and returns canonical timezone name', async () => {
@ -113,17 +103,16 @@ test('can select a timezone values and returns canonical timezone name', async (
timezone="Africa/Abidjan"
/>,
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
openSelectMenu();
const searchInput = screen.getByRole('combobox');
// search for mountain time
await userEvent.type(searchInput, 'mou');
userEvent.type(searchInput, 'mou');
const findTitle = 'GMT -07:00 (Mountain Standard Time)';
const selectOption = await screen.findByTitle(findTitle);
userEvent.click(selectOption);
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Cambridge_Bay');
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Boise');
});
test('can update props and rerender with different values', async () => {
@ -135,14 +124,13 @@ test('can update props and rerender with different values', async () => {
timezone="Asia/Dubai"
/>,
);
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
expect(screen.getByTitle('GMT +04:00 (Asia/Dubai)')).toBeInTheDocument();
expect(screen.getByTitle('GMT +04:00 (Asia/Baku)')).toBeInTheDocument();
rerender(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="Australia/Perth"
/>,
);
expect(screen.getByTitle('GMT +08:00 (Australia/Perth)')).toBeInTheDocument();
expect(onTimezoneChange).toHaveBeenCalledTimes(0);
expect(screen.getByTitle('GMT +08:00 (Asia/Brunei)')).toBeInTheDocument();
expect(onTimezoneChange).toHaveBeenCalledTimes(2);
});

View File

@ -17,10 +17,10 @@
* under the License.
*/
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo } from 'react';
import { t } from '@superset-ui/core';
import { Select } from 'src/components';
import Loading from 'src/components/Loading';
import { isDST, extendedDayjs } from 'src/utils/dates';
const DEFAULT_TIMEZONE = {
name: 'GMT Standard Time',
@ -56,24 +56,11 @@ export default function TimezoneSelector({
timezone,
minWidth = MIN_SELECT_WIDTH, // smallest size for current values
}: TimezoneSelectorProps) {
const [momentLib, setMomentLib] = useState<
typeof import('moment-timezone') | null
>(null);
useEffect(() => {
import('moment-timezone').then(momentLib =>
setMomentLib(() => momentLib.default),
);
}, []);
const { TIMEZONE_OPTIONS, TIMEZONE_OPTIONS_SORT_COMPARATOR, validTimezone } =
useMemo(() => {
if (!momentLib) {
return {};
}
const currentDate = momentLib();
const JANUARY = momentLib([2021, 1]);
const JULY = momentLib([2021, 7]);
const currentDate = extendedDayjs();
const JANUARY = extendedDayjs.tz('2021-01-01');
const JULY = extendedDayjs.tz('2021-07-01');
const getOffsetKey = (name: string) =>
JANUARY.tz(name).utcOffset().toString() +
@ -82,43 +69,41 @@ export default function TimezoneSelector({
const getTimezoneName = (name: string) => {
const offsets = getOffsetKey(name);
return (
(currentDate.tz(name).isDST()
(isDST(currentDate.tz(name), name)
? offsetsToName[offsets]?.[1]
: offsetsToName[offsets]?.[0]) || name
);
};
const ALL_ZONES = momentLib.tz
.countries()
.map(country => momentLib.tz.zonesForCountry(country, true))
.flat();
const dedupedTimezones = new Map();
// TODO: remove this ts-ignore when typescript is upgraded to 5.1
// @ts-ignore
const ALL_ZONES: string[] = Intl.supportedValuesOf('timeZone');
const TIMEZONES: import('moment-timezone').MomentZoneOffset[] = [];
ALL_ZONES.forEach(zone => {
if (
!TIMEZONES.find(
option => getOffsetKey(option.name) === getOffsetKey(zone.name),
)
) {
TIMEZONES.push(zone); // dedupe zones by offsets
const offsetKey = getOffsetKey(zone);
if (!dedupedTimezones.has(offsetKey)) {
dedupedTimezones.set(offsetKey, zone);
}
});
const TIMEZONES: string[] = Array.from(dedupedTimezones.values());
const TIMEZONE_OPTIONS = TIMEZONES.map(zone => ({
label: `GMT ${momentLib
.tz(currentDate, zone.name)
.format('Z')} (${getTimezoneName(zone.name)})`,
value: zone.name,
offsets: getOffsetKey(zone.name),
timezoneName: zone.name,
label: `GMT ${extendedDayjs
.tz(currentDate, zone)
.format('Z')} (${getTimezoneName(zone)})`,
value: zone,
offsets: getOffsetKey(zone),
timezoneName: zone,
}));
const TIMEZONE_OPTIONS_SORT_COMPARATOR = (
a: (typeof TIMEZONE_OPTIONS)[number],
b: (typeof TIMEZONE_OPTIONS)[number],
) =>
momentLib.tz(currentDate, a.timezoneName).utcOffset() -
momentLib.tz(currentDate, b.timezoneName).utcOffset();
extendedDayjs.tz(currentDate, a.timezoneName).utcOffset() -
extendedDayjs.tz(currentDate, b.timezoneName).utcOffset();
// pre-sort timezone options by time offset
TIMEZONE_OPTIONS.sort(TIMEZONE_OPTIONS_SORT_COMPARATOR);
@ -129,7 +114,7 @@ export default function TimezoneSelector({
)?.value || DEFAULT_TIMEZONE.value;
const validTimezone = matchTimezoneToOptions(
timezone || momentLib.tz.guess(),
timezone || extendedDayjs.tz.guess(),
);
return {
@ -137,7 +122,7 @@ export default function TimezoneSelector({
TIMEZONE_OPTIONS_SORT_COMPARATOR,
validTimezone,
};
}, [momentLib, timezone]);
}, [timezone]);
// force trigger a timezone update if provided `timezone` is not invalid
useEffect(() => {
@ -146,10 +131,6 @@ export default function TimezoneSelector({
}
}, [validTimezone, onTimezoneChange, timezone]);
if (!TIMEZONE_OPTIONS || !TIMEZONE_OPTIONS_SORT_COMPARATOR) {
return <Loading position="inline-centered" />;
}
return (
<Select
ariaLabel={t('Timezone selector')}

View File

@ -17,7 +17,7 @@
* under the License.
*/
/* eslint-env browser */
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
styled,
@ -258,7 +258,9 @@ const Header = () => {
if (predefinedValue) {
intervalMessage = t(predefinedValue[1]);
} else {
intervalMessage = moment.duration(interval, 'millisecond').humanize();
intervalMessage = extendedDayjs
.duration(interval, 'millisecond')
.humanize();
}
}

View File

@ -17,7 +17,7 @@
* under the License.
*/
import { Fragment, useMemo, useCallback, RefObject, createRef } from 'react';
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import { useDispatch } from 'react-redux';
import ReactDiffViewer from 'react-diff-viewer-continued';
import { useInView } from 'react-intersection-observer';
@ -179,7 +179,9 @@ const OverrideConfirmModal = ({ overwriteConfirmMetadata }: Props) => {
newValue={newValue}
leftTitle={t(
'Last Updated %s by %s',
moment.utc(overwriteConfirmMetadata.updatedAt).calendar(),
extendedDayjs
.utc(overwriteConfirmMetadata.updatedAt)
.calendar(),
overwriteConfirmMetadata.updatedBy,
)}
rightTitle="new value"

View File

@ -19,7 +19,7 @@
import { MouseEvent, Key, useState, useRef, RefObject } from 'react';
import { useHistory } from 'react-router-dom';
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import {
Behavior,
css,
@ -257,9 +257,11 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => {
const isTable = slice.viz_type === VizType.Table;
const isPivotTable = slice.viz_type === VizType.PivotTable;
const cachedWhen = (cachedDttm || []).map(itemCachedDttm =>
moment.utc(itemCachedDttm).fromNow(),
extendedDayjs.utc(itemCachedDttm).fromNow(),
);
const updatedWhen = updatedDttm ? moment.utc(updatedDttm).fromNow() : '';
const updatedWhen = updatedDttm
? extendedDayjs.utc(updatedDttm).fromNow()
: '';
const getCachedTitle = (itemCached: boolean) => {
if (itemCached) {
return t('Cached %s', cachedWhen);

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
// TODO: @msyavuz - Replace this with dayjs after datepicker migration
import moment, { Moment } from 'moment';
import { CustomRangeType } from 'src/explore/components/controls/DateFilterControl/types';
import { MOMENT_FORMAT } from './constants';

View File

@ -18,6 +18,7 @@
*/
import { ReactNode, useCallback, useEffect, useState } from 'react';
import { isEmpty, isEqual } from 'lodash';
// TODO: @msyavuz - Replace with dayjs when migrating datpicker to antd5
import moment, { Moment } from 'moment';
import {
parseDttmToDate,

View File

@ -18,13 +18,7 @@
*/
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import {
render,
screen,
waitFor,
within,
waitForElementToBeRemoved,
} from 'spec/helpers/testing-library';
import { render, screen, within } from 'spec/helpers/testing-library';
import { VizType } from '@superset-ui/core';
import { buildErrorTooltipMessage } from './buildErrorTooltipMessage';
import AlertReportModal, { AlertReportModalProps } from './AlertReportModal';
@ -193,10 +187,8 @@ const comboboxSelect = async (
) => {
expect(element).toBeInTheDocument();
userEvent.type(element, `${value}{enter}`);
await waitFor(() => {
const element = newElementQuery();
expect(element).toBeInTheDocument();
});
const newElement = newElementQuery();
expect(newElement).toBeInTheDocument();
};
// --------------- TEST SECTION ------------------
@ -526,7 +518,6 @@ test('renders default Schedule fields', async () => {
useRedux: true,
});
userEvent.click(screen.getByTestId('schedule-panel'));
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading'));
const scheduleType = screen.getByRole('combobox', {
name: /schedule type/i,
});
@ -561,12 +552,13 @@ test('shows CRON Expression when CRON is selected', async () => {
useRedux: true,
});
userEvent.click(screen.getByTestId('schedule-panel'));
await comboboxSelect(
userEvent.click(screen.getByRole('combobox', { name: /schedule type/i }));
userEvent.type(
screen.getByRole('combobox', { name: /schedule type/i }),
'cron schedule',
() => screen.getByPlaceholderText(/cron expression/i),
'cron schedule{enter}',
);
expect(screen.getByPlaceholderText(/cron expression/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/cron expression/i)).toBeInTheDocument();
});
test('defaults to day when CRON is not selected', async () => {
render(<AlertReportModal {...generateMockedProps(true, false, false)} />, {

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import { t, styled } from '@superset-ui/core';
import TableView, { EmptyWrapperType } from 'src/components/TableView';
import { TagsList } from 'src/components/Tags';
@ -54,7 +54,7 @@ interface TaggedObject {
type: string;
name: string;
url: string;
changed_on: moment.MomentInput;
changed_on: string | number | Date;
created_by: number | undefined;
creator: string;
owners: Owner[];
@ -89,7 +89,7 @@ export default function AllEntitiesTable({
const renderTable = (type: objectType) => {
const data = objects[type].map((o: TaggedObject) => ({
[type]: <a href={o.url}>{o.name}</a>,
modified: moment.utc(o.changed_on).fromNow(),
modified: extendedDayjs.utc(o.changed_on).fromNow(),
tags: o.tags,
owners: o.owners,
}));

View File

@ -21,6 +21,7 @@ import { FunctionComponent, useState, useEffect, ChangeEvent } from 'react';
import { styled, t } from '@superset-ui/core';
import { useSingleViewResource } from 'src/views/CRUD/hooks';
import { RangePicker } from 'src/components/DatePicker';
// TODO: @msyavuz - Remove this after datepicker
import moment from 'moment';
import Icons from 'src/components/Icons';
import Modal from 'src/components/Modal';

View File

@ -38,7 +38,7 @@ import Icons from 'src/components/Icons';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { FilterOperator } from 'src/components/ListView';
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import TruncatedList from 'src/components/TruncatedList';
interface DatasetUsageProps {
@ -92,7 +92,9 @@ const columns: ColumnsType<Chart> = [
sorter: true,
defaultSortOrder: 'descend',
render: (value, record) =>
record.last_saved_at ? moment.utc(record.last_saved_at).fromNow() : null,
record.last_saved_at
? extendedDayjs.utc(record.last_saved_at).fromNow()
: null,
},
{
key: 'last_saved_by.first_name',

View File

@ -17,7 +17,7 @@
* under the License.
*/
import { useEffect, useState } from 'react';
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import { styled, t } from '@superset-ui/core';
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { Link } from 'react-router-dom';
@ -112,13 +112,16 @@ const getEntityUrl = (entity: ActivityObject) => {
const getEntityLastActionOn = (entity: ActivityObject) => {
if ('time' in entity) {
return t('Viewed %s', moment(entity.time).fromNow());
return t('Viewed %s', extendedDayjs(entity.time).fromNow());
}
let time: number | string | undefined | null;
if ('changed_on' in entity) time = entity.changed_on;
if ('changed_on_utc' in entity) time = entity.changed_on_utc;
return t('Modified %s', time == null ? UNKNOWN_TIME : moment(time).fromNow());
return t(
'Modified %s',
time == null ? UNKNOWN_TIME : extendedDayjs(time).fromNow(),
);
};
export default function ActivityTable({

View File

@ -26,7 +26,7 @@ import {
styled,
getExtensionsRegistry,
} from '@superset-ui/core';
import moment from 'moment';
import { extendedDayjs } from 'src/utils/dates';
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
import FacePile from 'src/components/FacePile';
import { Tooltip } from 'src/components/Tooltip';
@ -263,7 +263,10 @@ function AlertList({
},
}: any) =>
lastEvalDttm
? moment.utc(lastEvalDttm).local().format(DATETIME_WITH_TIME_ZONE)
? extendedDayjs
.utc(lastEvalDttm)
.local()
.format(DATETIME_WITH_TIME_ZONE)
: '',
accessor: 'last_eval_dttm',
Header: t('Last run'),

View File

@ -36,13 +36,14 @@ import withToasts, { useToasts } from 'src/components/MessageToasts/withToasts';
import { fetchObjectsByTagIds, fetchSingleTag } from 'src/features/tags/tags';
import Loading from 'src/components/Loading';
import getOwnerName from 'src/utils/getOwnerName';
import { ConfigType } from 'dayjs';
interface TaggedObject {
id: number;
type: string;
name: string;
url: string;
changed_on: moment.MomentInput;
changed_on: ConfigType;
created_by: number | undefined;
creator: string;
owners: Owner[];

View File

@ -26,7 +26,7 @@ import {
SupersetClient,
getClientErrorObject,
} from '@superset-ui/core';
import moment from 'moment';
import dayjs from 'dayjs';
import rison from 'rison';
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
@ -170,7 +170,9 @@ function AnnotationList({
row: {
original: { start_dttm: startDttm },
},
}: any) => moment(new Date(startDttm)).format('ll'),
}: {
row: { original: AnnotationObject };
}) => dayjs(new Date(startDttm)).format('ll'),
Header: t('Start'),
accessor: 'start_dttm',
},
@ -179,12 +181,18 @@ function AnnotationList({
row: {
original: { end_dttm: endDttm },
},
}: any) => moment(new Date(endDttm)).format('ll'),
}: {
row: { original: AnnotationObject };
}) => dayjs(new Date(endDttm)).format('ll'),
Header: t('End'),
accessor: 'end_dttm',
},
{
Cell: ({ row: { original } }: any) => {
Cell: ({
row: { original },
}: {
row: { original: AnnotationObject };
}) => {
const handleEdit = () => handleAnnotationEdit(original);
const handleDelete = () => setAnnotationCurrentlyDeleting(original);
const actions = [

View File

@ -18,7 +18,7 @@
*/
import { css, styled, t } from '@superset-ui/core';
import moment from 'moment';
import dayjs from 'dayjs';
import { useEffect, useMemo } from 'react';
import { Link, useParams } from 'react-router-dom';
import ListView from 'src/components/ListView';
@ -32,6 +32,7 @@ import {
useSingleViewResource,
} from 'src/views/CRUD/hooks';
import { AlertObject, LogObject } from 'src/features/alerts/types';
import { AnnotationObject } from 'src/features/annotations/types';
const PAGE_SIZE = 25;
@ -119,7 +120,7 @@ function ExecutionLog({
original: { scheduled_dttm: scheduledDttm },
},
}: any) =>
moment(new Date(scheduledDttm)).format('YYYY-MM-DD hh:mm:ss a'),
dayjs(new Date(scheduledDttm)).format('YYYY-MM-DD hh:mm:ss a'),
accessor: 'scheduled_dttm',
Header: t('Scheduled at (UTC)'),
},
@ -128,7 +129,9 @@ function ExecutionLog({
row: {
original: { start_dttm: startDttm },
},
}: any) => moment(new Date(startDttm)).format('YYYY-MM-DD hh:mm:ss a'),
}: {
row: { original: AnnotationObject };
}) => dayjs(new Date(startDttm)).format('YYYY-MM-DD hh:mm:ss a'),
Header: t('Start at (UTC)'),
accessor: 'start_dttm',
},
@ -137,7 +140,9 @@ function ExecutionLog({
row: {
original: { start_dttm: startDttm, end_dttm: endDttm },
},
}: any) =>
}: {
row: { original: AnnotationObject };
}) =>
fDuration(new Date(startDttm).getTime(), new Date(endDttm).getTime()),
Header: t('Duration'),
disableSortBy: true,

View File

@ -25,7 +25,6 @@ import {
t,
useTheme,
} from '@superset-ui/core';
import moment from 'moment';
import {
createFetchRelated,
createFetchDistinct,
@ -54,6 +53,7 @@ import Icons from 'src/components/Icons';
import QueryPreviewModal from 'src/features/queries/QueryPreviewModal';
import { addSuccessToast } from 'src/components/MessageToasts/actions';
import getOwnerName from 'src/utils/getOwnerName';
import { extendedDayjs } from 'src/utils/dates';
const PAGE_SIZE = 25;
const SQL_PREVIEW_MAX_LINES = 4;
@ -214,7 +214,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
original: { start_time },
},
}: any) => {
const startMoment = moment.utc(start_time).local();
const startMoment = extendedDayjs.utc(start_time).local();
const formattedStartTimeData = startMoment
.format(DATETIME_WITH_TIME_ZONE)
.split(' ');
@ -238,7 +238,9 @@ function QueryList({ addDangerToast }: QueryListProps) {
}: any) => {
const timerType = status === QueryState.Failed ? 'danger' : status;
const timerTime = end_time
? moment(moment.utc(end_time - start_time)).format(TIME_WITH_MS)
? extendedDayjs(extendedDayjs.utc(end_time - start_time)).format(
TIME_WITH_MS,
)
: '00:00:00.000';
return (
<TimerLabel type={timerType} role="timer">

View File

@ -18,7 +18,7 @@
*/
import { setConfig as setHotLoaderConfig } from 'react-hot-loader';
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
import moment from 'moment';
import dayjs from 'dayjs';
// eslint-disable-next-line no-restricted-imports
import {
configure,
@ -44,7 +44,7 @@ const bootstrapData = getBootstrapData();
// Configure translation
if (typeof window !== 'undefined') {
configure({ languagePack: bootstrapData.common.language_pack });
moment.locale(bootstrapData.common.locale);
dayjs.locale(bootstrapData.common.locale);
} else {
configure();
}

View File

@ -1,42 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import moment from 'moment';
export const fDuration = function (t1, t2, format = 'HH:mm:ss.SS') {
const diffSec = t2 - t1;
const duration = moment(new Date(diffSec));
return duration.utc().format(format);
};
export const now = function () {
// seconds from EPOCH as a float
return moment().utc().valueOf();
};
export const epochTimeXHoursAgo = function (h) {
return moment().subtract(h, 'hours').utc().valueOf();
};
export const epochTimeXDaysAgo = function (d) {
return moment().subtract(d, 'days').utc().valueOf();
};
export const epochTimeXYearsAgo = function (y) {
return moment().subtract(y, 'years').utc().valueOf();
};

View File

@ -22,20 +22,41 @@ import {
epochTimeXHoursAgo,
epochTimeXDaysAgo,
epochTimeXYearsAgo,
extendedDayjs,
} from 'src/utils/dates';
describe('extendedDayjs', () => {
it('returns dayjs object with extended methods', () => {
const dayjs = extendedDayjs();
expect(dayjs).toHaveProperty('utc');
expect(dayjs).toHaveProperty('calendar');
expect(dayjs).toHaveProperty('tz');
expect(dayjs).toHaveProperty('fromNow');
expect(
extendedDayjs(
'05/02/69 1:02:03 PM -05:00',
'MM/DD/YY H:mm:ss A Z',
).toISOString(),
).toEqual('1969-05-02T18:02:03.000Z');
expect(extendedDayjs).toHaveProperty('duration');
expect(extendedDayjs).toHaveProperty('updateLocale');
});
});
describe('fDuration', () => {
it('is a function', () => {
expect(typeof fDuration).toBe('function');
});
it('returns a string', () => {
expect(typeof fDuration(new Date(), new Date())).toBe('string');
expect(typeof fDuration(new Date().getTime(), new Date().getTime())).toBe(
'string',
);
});
it('returns the expected output', () => {
const output = fDuration('1496293608897', '1496293623406');
expect(output).toBe('00:00:14.50');
const output = fDuration(1496293608897, 1496293623406);
expect(output).toBe('00:00:14.509');
});
});

View File

@ -0,0 +1,69 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import dayjs, { Dayjs } from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import calendar from 'dayjs/plugin/calendar';
import relativeTime from 'dayjs/plugin/relativeTime';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import duration from 'dayjs/plugin/duration';
import updateLocale from 'dayjs/plugin/updateLocale';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(calendar);
dayjs.extend(relativeTime);
dayjs.extend(customParseFormat);
dayjs.extend(duration);
dayjs.extend(updateLocale);
export const extendedDayjs = dayjs;
export const fDuration = function (
t1: number,
t2: number,
format = 'HH:mm:ss.SSS',
): string {
const diffSec = t2 - t1;
const duration = dayjs(new Date(diffSec));
return duration.utc().format(format);
};
export const now = function (): number {
// seconds from EPOCH as a float
return dayjs().utc().valueOf();
};
export const epochTimeXHoursAgo = function (h: number): number {
return dayjs().subtract(h, 'hours').utc().valueOf();
};
export const epochTimeXDaysAgo = function (d: number): number {
return dayjs().subtract(d, 'days').utc().valueOf();
};
export const epochTimeXYearsAgo = function (y: number): number {
return dayjs().subtract(y, 'years').utc().valueOf();
};
export const isDST = function (date: Dayjs, timezoneName: string): boolean {
const standardOffset = dayjs.tz('2021-01-01', timezoneName).utcOffset();
const currentOffset = date.tz(timezoneName).utcOffset();
return currentOffset !== standardOffset;
};

View File

@ -18,7 +18,6 @@
* under the License.
*/
import { ReactNode } from 'react';
import moment from 'moment';
import {
formatNumber,
formatTime,
@ -35,6 +34,7 @@ import {
XYChart,
buildChartTheme,
} from '@visx/xychart';
import { extendedDayjs } from 'src/utils/dates';
interface Props {
ariaLabel: string;
@ -223,7 +223,7 @@ const SparklineCell = ({
{idx !== undefined &&
formatTime(
dateFormat,
moment.utc(entries[idx].time).toDate(),
extendedDayjs.utc(entries[idx].time).toDate(),
)}
</div>
</div>

View File

@ -58,7 +58,6 @@ const getAvailableTranslationCodes = () => {
.map(dirName => dirName.replace('_', '-'))
.map(dirName => LOCALE_CODE_MAPPING[dirName] || dirName);
}
// Indicates to the MomentLocalesPlugin that we only want to keep 'en'.
return [];
};