From b5e62753b7fabfe3966249931ebcfe96abe0fd38 Mon Sep 17 00:00:00 2001 From: Mehmet Salih Yavuz Date: Mon, 13 Jan 2025 17:02:17 +0300 Subject: [PATCH] refactor(date picker): Migrate Date Picker to Ant Design 5 (#31019) Co-authored-by: Geido <60598000+geido@users.noreply.github.com> --- superset-frontend/package-lock.json | 180 +++------- superset-frontend/package.json | 3 - .../src/constants.ts | 2 +- .../components/AntdThemeProvider/index.tsx | 12 +- .../DatePicker/DatePicker.stories.tsx | 40 ++- .../components/DatePicker/DatePicker.test.tsx | 31 ++ .../src/components/DatePicker/index.tsx | 10 +- .../components/ListView/Filters/DateRange.tsx | 60 ++-- .../src/components/ListView/ListView.test.jsx | 15 +- ...mezoneSelector.DaylightSavingTime.test.tsx | 4 +- .../TimezoneSelector.test.tsx | 2 +- .../src/components/TimezoneSelector/index.tsx | 38 +- .../controls/ComparisonRangeLabel.tsx | 16 +- .../components/CustomFrame.tsx | 338 ++++++++---------- .../tests/CustomFrame.test.tsx | 7 +- .../DateFilterControl/utils/constants.ts | 30 +- .../DateFilterControl/utils/dateParser.ts | 16 +- .../controls/TimeOffsetControl.test.tsx | 6 +- .../components/controls/TimeOffsetControl.tsx | 54 +-- .../features/annotations/AnnotationModal.tsx | 34 +- superset-frontend/src/hooks/useLocale.ts | 83 +++++ .../src/pages/QueryHistoryList/index.tsx | 4 +- superset-frontend/src/theme/index.ts | 5 + superset-frontend/src/utils/common.js | 2 +- superset-frontend/src/utils/dates.ts | 7 + superset-frontend/webpack.config.js | 22 -- 26 files changed, 532 insertions(+), 489 deletions(-) create mode 100644 superset-frontend/src/components/DatePicker/DatePicker.test.tsx create mode 100644 superset-frontend/src/hooks/useLocale.ts diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 6b1808afa8f..18041a1f25e 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -94,8 +94,6 @@ "markdown-to-jsx": "^7.7.2", "match-sorter": "^6.3.4", "memoize-one": "^5.2.1", - "moment": "^2.30.1", - "moment-timezone": "^0.5.44", "mousetrap": "^1.6.5", "mustache": "^4.2.0", "nanoid": "^5.0.9", @@ -270,7 +268,6 @@ "less-loader": "^12.2.0", "mini-css-extract-plugin": "^2.9.0", "mock-socket": "^9.3.1", - "moment-locales-webpack-plugin": "^1.2.0", "node-fetch": "^2.6.7", "open-cli": "^8.0.0", "po2json": "^0.4.5", @@ -16395,6 +16392,45 @@ "react-dom": ">=16.9.0" } }, + "node_modules/antd-v5/node_modules/rc-picker": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.5.0.tgz", + "integrity": "sha512-suqz9bzuhBQlf7u+bZd1bJLPzhXpk12w6AjQ9BTPTiFwexVZgUKViG1KNLyfFvW6tCUZZK0HmCCX7JAyM+JnCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.38.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, "node_modules/antd-v5/node_modules/rc-progress": { "version": "4.0.0", "license": "MIT", @@ -34622,13 +34658,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.difference": { - "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, - "license": "MIT" - }, "node_modules/lodash.escape": { "version": "4.0.1", "dev": true, @@ -40430,30 +40459,6 @@ "node": "*" } }, - "node_modules/moment-locales-webpack-plugin": { - "version": "1.2.0", - "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" - }, - "peerDependencies": { - "moment": "^2.8.0", - "webpack": "^1 || ^2 || ^3 || ^4 || ^5" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.44", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/monaco-editor": { "version": "0.34.1", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.1.tgz", @@ -45395,57 +45400,6 @@ "react-dom": ">=16.9.0" } }, - "node_modules/rc-picker": { - "version": "4.5.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.1", - "rc-overflow": "^1.3.2", - "rc-resize-observer": "^1.4.0", - "rc-util": "^5.38.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "date-fns": ">= 2.x", - "dayjs": ">= 1.x", - "luxon": ">= 3.x", - "moment": ">= 2.x", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - }, - "peerDependenciesMeta": { - "date-fns": { - "optional": true - }, - "dayjs": { - "optional": true - }, - "luxon": { - "optional": true - }, - "moment": { - "optional": true - } - } - }, - "node_modules/rc-picker/node_modules/rc-resize-observer": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.7", - "classnames": "^2.2.1", - "rc-util": "^5.38.0", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/rc-progress": { "version": "3.1.1", "license": "MIT", @@ -72904,6 +72858,19 @@ "rc-util": "^5.38.0" } }, + "rc-picker": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.5.0.tgz", + "integrity": "sha512-suqz9bzuhBQlf7u+bZd1bJLPzhXpk12w6AjQ9BTPTiFwexVZgUKViG1KNLyfFvW6tCUZZK0HmCCX7JAyM+JnCg==", + "requires": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.38.1" + } + }, "rc-progress": { "version": "4.0.0", "requires": { @@ -84775,12 +84742,6 @@ "version": "4.0.8", "dev": true }, - "lodash.difference": { - "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 - }, "lodash.escape": { "version": "4.0.1", "dev": true @@ -87910,21 +87871,6 @@ "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", - "resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz", - "integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==", - "dev": true, - "requires": { - "lodash.difference": "^4.5.0" - } - }, - "moment-timezone": { - "version": "0.5.44", - "requires": { - "moment": "^2.29.4" - } - }, "monaco-editor": { "version": "0.34.1", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.1.tgz", @@ -91198,28 +91144,6 @@ "classnames": "^2.2.1" } }, - "rc-picker": { - "version": "4.5.0", - "requires": { - "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.1", - "rc-overflow": "^1.3.2", - "rc-resize-observer": "^1.4.0", - "rc-util": "^5.38.1" - }, - "dependencies": { - "rc-resize-observer": { - "version": "1.4.0", - "requires": { - "@babel/runtime": "^7.20.7", - "classnames": "^2.2.1", - "rc-util": "^5.38.0", - "resize-observer-polyfill": "^1.5.1" - } - } - } - }, "rc-progress": { "version": "3.1.1", "requires": { diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 8b72294c392..8f6e5160aae 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -161,8 +161,6 @@ "markdown-to-jsx": "^7.7.2", "match-sorter": "^6.3.4", "memoize-one": "^5.2.1", - "moment": "^2.30.1", - "moment-timezone": "^0.5.44", "mousetrap": "^1.6.5", "mustache": "^4.2.0", "nanoid": "^5.0.9", @@ -337,7 +335,6 @@ "less-loader": "^12.2.0", "mini-css-extract-plugin": "^2.9.0", "mock-socket": "^9.3.1", - "moment-locales-webpack-plugin": "^1.2.0", "node-fetch": "^2.6.7", "open-cli": "^8.0.0", "po2json": "^0.4.5", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts index 6534258c66f..c9e8bf9db7f 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts @@ -81,5 +81,5 @@ export const DEFAULT_XAXIS_SORT_SERIES_DATA: SortSeriesData = { export const DEFAULT_DATE_PATTERN = /\d{4}-\d{2}-\d{2}/g; -// When moment fails to parse a date +// When it fails to parse a date export const INVALID_DATE = 'Invalid date'; diff --git a/superset-frontend/src/components/AntdThemeProvider/index.tsx b/superset-frontend/src/components/AntdThemeProvider/index.tsx index 03cc8e2c488..28ddcabf34a 100644 --- a/superset-frontend/src/components/AntdThemeProvider/index.tsx +++ b/superset-frontend/src/components/AntdThemeProvider/index.tsx @@ -20,8 +20,16 @@ import { ConfigProvider, type ConfigProviderProps } from 'antd-v5'; import { getTheme, ThemeType } from 'src/theme/index'; -export const AntdThemeProvider = ({ theme, children }: ConfigProviderProps) => ( - +export const AntdThemeProvider = ({ + theme, + children, + ...rest +}: ConfigProviderProps) => ( + {children} ); diff --git a/superset-frontend/src/components/DatePicker/DatePicker.stories.tsx b/superset-frontend/src/components/DatePicker/DatePicker.stories.tsx index 69a2b689aeb..563f48f07f7 100644 --- a/superset-frontend/src/components/DatePicker/DatePicker.stories.tsx +++ b/superset-frontend/src/components/DatePicker/DatePicker.stories.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { DatePickerProps, RangePickerProps } from 'antd/lib/date-picker'; +import { DatePickerProps } from 'antd-v5'; +import { RangePickerProps } from 'antd-v5/es/date-picker'; import { DatePicker, RangePicker } from '.'; export default { @@ -24,14 +25,17 @@ export default { component: DatePicker, }; -const commonArgs = { - allowClear: true, +const commonArgs: DatePickerProps = { + allowClear: false, autoFocus: true, - bordered: true, disabled: false, - inputReadOnly: false, - size: 'middle', format: 'YYYY-MM-DD hh:mm a', + inputReadOnly: false, + order: true, + picker: 'date', + placement: 'bottomLeft', + size: 'middle', + showNow: true, showTime: { format: 'hh:mm a' }, }; @@ -49,6 +53,25 @@ const interactiveTypes = { }, options: ['large', 'middle', 'small'], }, + placement: { + control: { + type: 'select', + }, + options: ['bottomLeft', 'bottomRight', 'topLeft', 'topRight'], + }, + status: { + control: { + type: 'select', + }, + options: ['error', 'warning'], + }, + + variant: { + control: { + type: 'select', + }, + options: ['outlined', 'borderless', 'filled'], + }, }; export const InteractiveDatePicker = (args: DatePickerProps) => ( @@ -57,9 +80,9 @@ export const InteractiveDatePicker = (args: DatePickerProps) => ( InteractiveDatePicker.args = { ...commonArgs, - picker: 'date', placeholder: 'Placeholder', showToday: true, + showTime: { format: 'hh:mm a', needConfirm: false }, }; InteractiveDatePicker.argTypes = interactiveTypes; @@ -70,9 +93,8 @@ export const InteractiveRangePicker = (args: RangePickerProps) => ( InteractiveRangePicker.args = { ...commonArgs, - allowEmpty: true, - showNow: true, separator: '-', + showTime: { format: 'hh:mm a', needConfirm: false }, }; InteractiveRangePicker.argTypes = interactiveTypes; diff --git a/superset-frontend/src/components/DatePicker/DatePicker.test.tsx b/superset-frontend/src/components/DatePicker/DatePicker.test.tsx new file mode 100644 index 00000000000..463c3ace578 --- /dev/null +++ b/superset-frontend/src/components/DatePicker/DatePicker.test.tsx @@ -0,0 +1,31 @@ +/** + * 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 { render } from 'spec/helpers/testing-library'; +import { DatePicker, RangePicker } from '.'; + +test('should render date picker', () => { + const { container } = render(); + expect(container).toBeInTheDocument(); +}); + +test('should render range picker', () => { + const { container } = render(); + expect(container).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/components/DatePicker/index.tsx b/superset-frontend/src/components/DatePicker/index.tsx index 56c0458c4e3..6538ae9b6a3 100644 --- a/superset-frontend/src/components/DatePicker/index.tsx +++ b/superset-frontend/src/components/DatePicker/index.tsx @@ -16,13 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { DatePicker as AntdDatePicker } from 'antd'; -import { styled } from '@superset-ui/core'; - -const AntdRangePicker = AntdDatePicker.RangePicker; - -export const RangePicker = styled(AntdRangePicker)` - border-radius: ${({ theme }) => theme.gridUnit}px; -`; +import { DatePicker as AntdDatePicker } from 'antd-v5'; export const DatePicker = AntdDatePicker; +export const { RangePicker } = AntdDatePicker; diff --git a/superset-frontend/src/components/ListView/Filters/DateRange.tsx b/superset-frontend/src/components/ListView/Filters/DateRange.tsx index 8b9f122034c..bf03012eedf 100644 --- a/superset-frontend/src/components/ListView/Filters/DateRange.tsx +++ b/superset-frontend/src/components/ListView/Filters/DateRange.tsx @@ -24,11 +24,14 @@ 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'; import { FormLabel } from 'src/components/Form'; +import { extendedDayjs } from 'src/utils/dates'; +import { Dayjs } from 'dayjs'; +import Loading from 'src/components/Loading'; +import { AntdThemeProvider } from 'src/components/AntdThemeProvider'; +import { useLocale } from 'src/hooks/useLocale'; import { BaseFilter, FilterHandler } from './Base'; interface DateRangeFilterProps extends BaseFilter { @@ -51,11 +54,13 @@ function DateRangeFilter( ref: RefObject, ) { const [value, setValue] = useState(initialValue ?? null); - const momentValue = useMemo((): [Moment, Moment] | null => { + const dayjsValue = useMemo((): [Dayjs, Dayjs] | null => { if (!value || (Array.isArray(value) && !value.length)) return null; - return [moment(value[0]), moment(value[1])]; + return [extendedDayjs(value[0]), extendedDayjs(value[1])]; }, [value]); + const locale = useLocale(); + useImperativeHandle(ref, () => ({ clearFilter: () => { setValue(null); @@ -63,28 +68,33 @@ function DateRangeFilter( }, })); + if (locale === null) { + return ; + } return ( - - {Header} - { - if (!momentRange) { - setValue(null); - onSubmit([]); - return; - } - const changeValue = [ - momentRange[0]?.valueOf() ?? 0, - momentRange[1]?.valueOf() ?? 0, - ] as ValueState; - setValue(changeValue); - onSubmit(changeValue); - }} - /> - + + + {Header} + { + if (!dayjsRange?.[0]?.valueOf() || !dayjsRange?.[1]?.valueOf()) { + setValue(null); + onSubmit([]); + return; + } + const changeValue = [ + dayjsRange[0]?.valueOf() ?? 0, + dayjsRange[1]?.valueOf() ?? 0, + ] as ValueState; + setValue(changeValue); + onSubmit(changeValue); + }} + /> + + ); } diff --git a/superset-frontend/src/components/ListView/ListView.test.jsx b/superset-frontend/src/components/ListView/ListView.test.jsx index 64869ad67ac..5658f4069e8 100644 --- a/superset-frontend/src/components/ListView/ListView.test.jsx +++ b/superset-frontend/src/components/ListView/ListView.test.jsx @@ -20,6 +20,8 @@ import { styledMount as mount } from 'spec/helpers/theming'; import { act } from 'react-dom/test-utils'; import { QueryParamProvider } from 'use-query-params'; import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; import Button from 'src/components/Button'; import { Empty } from 'src/components/EmptyState/Empty'; @@ -33,6 +35,10 @@ import TableCollection from 'src/components/TableCollection'; import Pagination from 'src/components/Pagination/Wrapper'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; +import { Provider } from 'react-redux'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); function makeMockLocation(query) { const queryStr = encodeURIComponent(query); @@ -125,12 +131,15 @@ const mockedProps = { const factory = (props = mockedProps) => mount( - - - , + + + + + , { wrappingComponent: ThemeProvider, wrappingComponentProps: { theme: supersetTheme }, + useRedux: true, }, ); diff --git a/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx b/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx index 3424cd73abe..4cafd197af1 100644 --- a/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx +++ b/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.DaylightSavingTime.test.tsx @@ -57,6 +57,6 @@ 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/Midway)'); - expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)'); - expect(options[3]).toHaveTextContent('GMT -09:30 (Pacific/Marquesas)'); + expect(options[2]).toHaveTextContent('GMT -11:00 (Pacific/Niue)'); + expect(options[3]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)'); }); diff --git a/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.test.tsx b/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.test.tsx index 4df8a1e550a..0f9309b5a8b 100644 --- a/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.test.tsx +++ b/superset-frontend/src/components/TimezoneSelector/TimezoneSelector.test.tsx @@ -91,7 +91,7 @@ test('render timezones in correct order for standard time', async () => { const options = await getSelectOptions(); expect(options[0]).toHaveTextContent('GMT -05:00 (Eastern Standard Time)'); expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Midway)'); - expect(options[2]).toHaveTextContent('GMT -10:00 (America/Adak)'); + expect(options[2]).toHaveTextContent('GMT -11:00 (Pacific/Niue)'); }); test('can select a timezone values and returns canonical timezone name', async () => { diff --git a/superset-frontend/src/components/TimezoneSelector/index.tsx b/superset-frontend/src/components/TimezoneSelector/index.tsx index 5a733ae376d..0584655b85c 100644 --- a/superset-frontend/src/components/TimezoneSelector/index.tsx +++ b/superset-frontend/src/components/TimezoneSelector/index.tsx @@ -75,28 +75,32 @@ export default function TimezoneSelector({ ); }; - 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'); - ALL_ZONES.forEach(zone => { - 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 ${extendedDayjs + const labels = new Set(); + const TIMEZONE_OPTIONS = ALL_ZONES.map(zone => { + const label = `GMT ${extendedDayjs .tz(currentDate, zone) - .format('Z')} (${getTimezoneName(zone)})`, - value: zone, - offsets: getOffsetKey(zone), - timezoneName: zone, - })); + .format('Z')} (${getTimezoneName(zone)})`; + + if (labels.has(label)) { + return null; // Skip duplicates + } + labels.add(label); + return { + label, + value: zone, + offsets: getOffsetKey(zone), + timezoneName: zone, + }; + }).filter(Boolean) as { + label: string; + value: string; + offsets: string; + timezoneName: string; + }[]; const TIMEZONE_OPTIONS_SORT_COMPARATOR = ( a: (typeof TIMEZONE_OPTIONS)[number], diff --git a/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx b/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx index 4a7c91b90a0..0fe5c222b96 100644 --- a/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx +++ b/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx @@ -20,7 +20,6 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { isEmpty, isEqual } from 'lodash'; -import moment from 'moment'; import { BinaryAdhocFilter, css, @@ -36,8 +35,9 @@ import ControlHeader, { } from 'src/explore/components/ControlHeader'; import { RootState } from 'src/views/store'; import { DEFAULT_DATE_PATTERN } from '@superset-ui/chart-controls'; +import { extendedDayjs } from 'src/utils/dates'; -const MOMENT_FORMAT = 'YYYY-MM-DD'; +const DAYJS_FORMAT = 'YYYY-MM-DD'; const isTimeRangeEqual = ( left: BinaryAdhocFilter[], @@ -104,8 +104,8 @@ export const ComparisonRangeLabel = ({ let useStartDate = startDate; if (!startDate && !isEmpty(previousCustomFilter)) { useStartDate = previousCustomFilter[0]?.comparator.split(' : ')[0]; - useStartDate = moment(parseDttmToDate(useStartDate)).format( - MOMENT_FORMAT, + useStartDate = extendedDayjs(parseDttmToDate(useStartDate)).format( + DAYJS_FORMAT, ); } const promises = currentTimeRangeFilters.map(filter => { @@ -136,10 +136,12 @@ export const ComparisonRangeLabel = ({ const dates = res?.value?.match(DEFAULT_DATE_PATTERN); const [parsedStartDate, parsedEndDate] = dates ?? []; if (parsedStartDate) { - const parsedDateMoment = moment(parseDttmToDate(parsedStartDate)); - const startDateMoment = moment(parseDttmToDate(startDate)); + const parsedDateDayjs = extendedDayjs( + parseDttmToDate(parsedStartDate), + ); + const startDateDayjs = extendedDayjs(parseDttmToDate(startDate)); if ( - startDateMoment.isSameOrBefore(parsedDateMoment) || + startDateDayjs.isSameOrBefore(parsedDateDayjs) || !startDate ) { const postProcessedShifts = getTimeOffset({ diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/CustomFrame.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/components/CustomFrame.tsx index 7a880a82953..ec558c09098 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/components/CustomFrame.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/CustomFrame.tsx @@ -16,10 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import type { PickerLocale } from 'antd/lib/date-picker/generatePicker'; -import { Moment } from 'moment'; import { isInteger } from 'lodash'; import { t, customTimeRangeDecode } from '@superset-ui/core'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; @@ -33,24 +29,23 @@ import { SINCE_MODE_OPTIONS, UNTIL_GRAIN_OPTIONS, UNTIL_MODE_OPTIONS, - MOMENT_FORMAT, + DAYJS_FORMAT, MIDNIGHT, customTimeRangeEncode, - dttmToMoment, - LOCALE_MAPPING, + dttmToDayjs, } from 'src/explore/components/controls/DateFilterControl/utils'; import { CustomRangeKey, FrameComponentProps, } from 'src/explore/components/controls/DateFilterControl/types'; -import { ExplorePageState } from 'src/explore/types'; import Loading from 'src/components/Loading'; +import { Dayjs } from 'dayjs'; +import { AntdThemeProvider } from 'src/components/AntdThemeProvider'; +import { useLocale } from 'src/hooks/useLocale'; export function CustomFrame(props: FrameComponentProps) { const { customRange, matchedFlag } = customTimeRangeDecode(props.value); - const [datePickerLocale, setDatePickerLocale] = useState< - PickerLocale | undefined | null - >(null); + const datePickerLocale = useLocale(); if (!matchedFlag) { props.onChange(customTimeRangeEncode(customRange)); } @@ -112,192 +107,173 @@ export function CustomFrame(props: FrameComponentProps) { } } - // check if there is a locale defined for explore - const localFromFlaskBabel = useSelector( - (state: ExplorePageState) => state?.common?.locale, - ); - - // An undefined datePickerLocale is acceptable if no match is found in the LOCALE_MAPPING[localFromFlaskBabel] lookup - // and will fall back to antd's default locale when the antd DataPicker's prop locale === undefined - // This also protects us from the case where state is populated with a locale that antd locales does not recognize - useEffect(() => { - if (datePickerLocale === null) { - if (localFromFlaskBabel && LOCALE_MAPPING[localFromFlaskBabel]) { - LOCALE_MAPPING[localFromFlaskBabel]() - .then((locale: { default: PickerLocale }) => - setDatePickerLocale(locale.default), - ) - .catch(() => setDatePickerLocale(undefined)); - } else { - setDatePickerLocale(undefined); - } - } - }, [datePickerLocale, localFromFlaskBabel]); - if (datePickerLocale === null) { return ; } return ( -
-
{t('Configure custom time range')}
- - -
- {t('START (INCLUSIVE)')}{' '} - -
- onChange('sinceGrain', value)} - /> - -
- )} - - -
- {t('END (EXCLUSIVE)')}{' '} - + onChange('untilMode', value)} - /> - {untilMode === 'specific' && ( - - - onChange('untilDatetime', datetime.format(MOMENT_FORMAT)) - } - allowClear={false} - locale={datePickerLocale} - getPopupContainer={triggerNode => - props.isOverflowingFilterBar - ? (triggerNode.parentNode as HTMLElement) - : document.body - } - /> - - )} - {untilMode === 'relative' && ( - - - - onGrainValue('untilGrainValue', value || 1) - } - onStep={value => onGrainValue('untilGrainValue', value || 1)} - /> - - - onChange('sinceGrain', value)} + /> + + + )} + + +
+ {t('END (EXCLUSIVE)')}{' '} + +
+ onChange('untilGrain', value)} + /> + + + )} + + + {sinceMode === 'relative' && untilMode === 'relative' && ( +
+
{t('Anchor to')}
+ + + + + {t('NOW')} + + + {t('Date/Time')} + + + + {anchorMode !== 'now' && ( + + + onChange('anchorValue', datetime.format(DAYJS_FORMAT)) + } + allowClear={false} + className="control-anchor-to-datetime" + getPopupContainer={(triggerNode: HTMLElement) => + props.isOverflowingFilterBar + ? (triggerNode.parentNode as HTMLElement) + : document.body + } + /> + + )} + +
+ )} +
+ ); } diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CustomFrame.test.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CustomFrame.test.tsx index e5ed701f32d..ab8fc5bb35b 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CustomFrame.test.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CustomFrame.test.tsx @@ -274,6 +274,7 @@ test('should translate Date Picker', async () => { await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading')); userEvent.click(screen.getAllByRole('img', { name: 'calendar' })[0]); expect(screen.getByText('2021')).toBeInTheDocument(); + expect(screen.getByText('lu')).toBeInTheDocument(); expect(screen.getByText('ma')).toBeInTheDocument(); expect(screen.getByText('me')).toBeInTheDocument(); @@ -306,7 +307,7 @@ test('calls onChange when START Specific Date/Time is selected', async () => { const randomDate = screen.getByTitle('2021-03-11'); userEvent.click(randomDate); - const okButton = screen.getByText('Ok'); + const okButton = screen.getByText('OK'); userEvent.click(okButton); expect(onChange).toHaveBeenCalled(); @@ -335,7 +336,7 @@ test('calls onChange when END Specific Date/Time is selected', async () => { const randomDate = screen.getByTitle('2021-03-28'); userEvent.click(randomDate); - const okButton = screen.getByText('Ok'); + const okButton = screen.getByText('OK'); userEvent.click(okButton); expect(onChange).toHaveBeenCalled(); @@ -372,7 +373,7 @@ test('calls onChange when a date is picked from anchor mode date picker', async const randomDate = screen.getByTitle('2024-06-05'); userEvent.click(randomDate); - const okButton = screen.getByText('Ok'); + const okButton = screen.getByText('OK'); userEvent.click(okButton); expect(onChange).toHaveBeenCalled(); diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts index dee66da7d99..4a116fb65e4 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import moment from 'moment'; import { t } from '@superset-ui/core'; import { SelectOptionType, @@ -32,6 +31,7 @@ import { CurrentQuarter, CurrentDay, } from 'src/explore/components/controls/DateFilterControl/types'; +import { extendedDayjs } from 'src/utils/dates'; export const FRAME_OPTIONS: SelectOptionType[] = [ { value: 'Common', label: t('Last') }, @@ -130,30 +130,16 @@ export const CURRENT_CALENDAR_RANGE_SET: Set = new Set([ CurrentYear, ]); -export const MOMENT_FORMAT = 'YYYY-MM-DD[T]HH:mm:ss'; -export const SEVEN_DAYS_AGO = moment() +export const DAYJS_FORMAT = 'YYYY-MM-DD[T]HH:mm:ss'; +export const SEVEN_DAYS_AGO = extendedDayjs() .utc() .startOf('day') .subtract(7, 'days') - .format(MOMENT_FORMAT); -export const MIDNIGHT = moment().utc().startOf('day').format(MOMENT_FORMAT); - -export const LOCALE_MAPPING = { - en: () => import('antd/lib/date-picker/locale/en_US'), - fr: () => import('antd/lib/date-picker/locale/fr_FR'), - es: () => import('antd/lib/date-picker/locale/es_ES'), - it: () => import('antd/lib/date-picker/locale/it_IT'), - zh: () => import('antd/lib/date-picker/locale/zh_CN'), - ja: () => import('antd/lib/date-picker/locale/ja_JP'), - de: () => import('antd/lib/date-picker/locale/de_DE'), - pt: () => import('antd/lib/date-picker/locale/pt_PT'), - pt_BR: () => import('antd/lib/date-picker/locale/pt_BR'), - ru: () => import('antd/lib/date-picker/locale/ru_RU'), - ko: () => import('antd/lib/date-picker/locale/ko_KR'), - sk: () => import('antd/lib/date-picker/locale/sk_SK'), - sl: () => import('antd/lib/date-picker/locale/sl_SI'), - nl: () => import('antd/lib/date-picker/locale/nl_NL'), -}; + .format(DAYJS_FORMAT); +export const MIDNIGHT = extendedDayjs() + .utc() + .startOf('day') + .format(DAYJS_FORMAT); export enum DateFilterTestKey { CommonFrame = 'common-frame', diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts index 07a946a3154..2f3256dd394 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -// TODO: @msyavuz - Replace this with dayjs after datepicker migration -import moment, { Moment } from 'moment'; +import { extendedDayjs } from 'src/utils/dates'; +import { Dayjs } from 'dayjs'; import { CustomRangeType } from 'src/explore/components/controls/DateFilterControl/types'; -import { MOMENT_FORMAT } from './constants'; +import { DAYJS_FORMAT } from './constants'; /** * RegExp to test a string for a full ISO 8601 Date @@ -39,18 +39,18 @@ export const ISO8601_AND_CONSTANT = RegExp( const SPECIFIC_MODE = ['specific', 'today', 'now']; -export const dttmToMoment = (dttm: string): Moment => { +export const dttmToDayjs = (dttm: string): Dayjs => { if (dttm === 'now') { - return moment().utc().startOf('second'); + return extendedDayjs().utc().startOf('second'); } if (dttm === 'today') { - return moment().utc().startOf('day'); + return extendedDayjs().utc().startOf('day'); } - return moment(dttm); + return extendedDayjs(dttm); }; export const dttmToString = (dttm: string): string => - dttmToMoment(dttm).format(MOMENT_FORMAT); + dttmToDayjs(dttm).format(DAYJS_FORMAT); export const customTimeRangeEncode = (customRange: CustomRangeType): string => { const { diff --git a/superset-frontend/src/explore/components/controls/TimeOffsetControl.test.tsx b/superset-frontend/src/explore/components/controls/TimeOffsetControl.test.tsx index 6745e34cf33..55facc590e4 100644 --- a/superset-frontend/src/explore/components/controls/TimeOffsetControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/TimeOffsetControl.test.tsx @@ -21,8 +21,8 @@ import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { Provider } from 'react-redux'; import { ThemeProvider, supersetTheme } from '@superset-ui/core'; -import moment from 'moment'; import { INVALID_DATE } from '@superset-ui/chart-controls'; +import { extendedDayjs } from 'src/utils/dates'; import TimeOffsetControls, { TimeOffsetControlsProps, } from './TimeOffsetControl'; @@ -73,8 +73,8 @@ describe('TimeOffsetControls', () => { // Our Time comparison control depends on this string for supporting date deletion on date picker // That's why this test is linked to the TimeOffsetControl component - it('Moment should return "Invalid date" when parsing an invalid date string', () => { - const invalidDate = moment('not-a-date'); + it('Dayjs should return "Invalid date" when parsing an invalid date string', () => { + const invalidDate = extendedDayjs('not-a-date'); expect(invalidDate.format()).toBe(INVALID_DATE); }); }); diff --git a/superset-frontend/src/explore/components/controls/TimeOffsetControl.tsx b/superset-frontend/src/explore/components/controls/TimeOffsetControl.tsx index c799db64f36..f85872104c6 100644 --- a/superset-frontend/src/explore/components/controls/TimeOffsetControl.tsx +++ b/superset-frontend/src/explore/components/controls/TimeOffsetControl.tsx @@ -18,8 +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 { extendedDayjs } from 'src/utils/dates'; import { parseDttmToDate, BinaryAdhocFilter, @@ -29,8 +28,8 @@ import { computeCustomDateTime, fetchTimeRange, } from '@superset-ui/core'; -import { DatePicker } from 'antd'; -import { RangePickerProps } from 'antd/lib/date-picker'; +import { DatePicker } from 'src/components/DatePicker'; +import { RangePickerProps } from 'antd-v5/es/date-picker'; import { useSelector } from 'react-redux'; import ControlHeader from 'src/explore/components/ControlHeader'; @@ -39,16 +38,17 @@ import { DEFAULT_DATE_PATTERN, INVALID_DATE, } from '@superset-ui/chart-controls'; +import { Dayjs } from 'dayjs'; export interface TimeOffsetControlsProps { label?: ReactNode; startDate?: string; description?: string; hovered?: boolean; - value?: Moment; + value?: Dayjs; onChange: (datetime: string) => void; } -const MOMENT_FORMAT = 'YYYY-MM-DD'; +const DAYJS_FORMAT = 'YYYY-MM-DD'; const isTimeRangeEqual = ( left: BinaryAdhocFilter[], @@ -62,14 +62,14 @@ export default function TimeOffsetControls({ ...props }: TimeOffsetControlsProps) { const [startDate, setStartDate] = useState(''); - const [formatedDate, setFormatedDate] = useState( + const [formatedDate, setFormatedDate] = useState( undefined, ); const [customStartDateInFilter, setCustomStartDateInFilter] = useState< - moment.Moment | undefined + Dayjs | undefined >(undefined); const [formatedFilterDate, setFormatedFilterDate] = useState< - moment.Moment | undefined + Dayjs | undefined >(undefined); const [savedStartDate, setSavedStartDate] = useState(null); const [isDateSelected, setIsDateSelected] = useState(true); @@ -92,7 +92,7 @@ export default function TimeOffsetControls({ if (savedStartDate !== currentStartDate) { setSavedStartDate(currentStartDate); if (currentStartDate !== INVALID_DATE) { - onChange(moment(currentStartDate).format(MOMENT_FORMAT)); + onChange(extendedDayjs(currentStartDate).format(DAYJS_FORMAT)); setIsDateSelected(true); } else { setIsDateSelected(false); @@ -132,7 +132,7 @@ export default function TimeOffsetControls({ ); } customStartDate?.setHours(0, 0, 0, 0); - setCustomStartDateInFilter(moment(customStartDate)); + setCustomStartDateInFilter(extendedDayjs(customStartDate)); } else { setCustomStartDateInFilter(undefined); } @@ -149,11 +149,11 @@ export default function TimeOffsetControls({ const dates = res?.value?.match(DEFAULT_DATE_PATTERN); const [startDate, endDate] = dates ?? []; customTimeRange(`${startDate} : ${endDate}` ?? ''); - setFormatedFilterDate(moment(parseDttmToDate(startDate))); + setFormatedFilterDate(extendedDayjs(parseDttmToDate(startDate))); }); } else { setCustomStartDateInFilter(undefined); - setFormatedFilterDate(moment(parseDttmToDate(''))); + setFormatedFilterDate(extendedDayjs(parseDttmToDate(''))); } }, [currentTimeRangeFilters, customTimeRange]); @@ -169,15 +169,15 @@ export default function TimeOffsetControls({ } if (customStartDateInFilter) { setStartDate(customStartDateInFilter.toString()); - setFormatedDate(moment(customStartDateInFilter)); + setFormatedDate(extendedDayjs(customStartDateInFilter)); } else if (date) { setStartDate(date); - setFormatedDate(moment(parseDttmToDate(date))); + setFormatedDate(extendedDayjs(parseDttmToDate(date))); } } else if (savedStartDate) { if (savedStartDate !== INVALID_DATE) { setStartDate(savedStartDate); - setFormatedDate(moment(parseDttmToDate(savedStartDate))); + setFormatedDate(extendedDayjs(parseDttmToDate(savedStartDate))); } } }, [previousCustomFilter, savedStartDate, customStartDateInFilter]); @@ -186,12 +186,12 @@ export default function TimeOffsetControls({ // When switching offsets from inherit and the previous custom is no longer valid if (customStartDateInFilter) { if (formatedDate && formatedDate > customStartDateInFilter) { - const resetDate = moment + const resetDate = extendedDayjs .utc(customStartDateInFilter) .subtract(1, 'day'); setStartDate(resetDate.toString()); setFormatedDate(resetDate); - onChange(moment.utc(resetDate).format(MOMENT_FORMAT)); + onChange(extendedDayjs.utc(resetDate).format(DAYJS_FORMAT)); setIsDateSelected(true); } } @@ -200,10 +200,12 @@ export default function TimeOffsetControls({ formatedFilterDate && formatedDate > formatedFilterDate ) { - const resetDate = moment.utc(formatedFilterDate).subtract(1, 'day'); + const resetDate = extendedDayjs + .utc(formatedFilterDate) + .subtract(1, 'day'); setStartDate(resetDate.toString()); setFormatedDate(resetDate); - onChange(moment.utc(resetDate).format(MOMENT_FORMAT)); + onChange(extendedDayjs.utc(resetDate).format(DAYJS_FORMAT)); setIsDateSelected(true); } }, [formatedFilterDate, formatedDate, customStartDateInFilter]); @@ -214,7 +216,7 @@ export default function TimeOffsetControls({ ? current && current > formatedFilterDate : false; } - return current && current > moment(customStartDateInFilter); + return current && current > extendedDayjs(customStartDateInFilter); }; return startDate || formatedDate ? ( @@ -224,15 +226,15 @@ export default function TimeOffsetControls({ css={css` width: 100%; `} - onChange={(datetime: Moment) => - onChange(datetime ? datetime.format(MOMENT_FORMAT) : '') + onChange={(datetime: Dayjs) => + onChange(datetime ? datetime.format(DAYJS_FORMAT) : '') } defaultPickerValue={ - startDate ? moment(formatedDate).subtract(1, 'day') : undefined + startDate ? extendedDayjs(formatedDate).subtract(1, 'day') : undefined } disabledDate={disabledDate} - defaultValue={moment(formatedDate)} - value={isDateSelected ? moment(formatedDate) : null} + defaultValue={extendedDayjs(formatedDate)} + value={isDateSelected ? extendedDayjs(formatedDate) : null} />
) : null; diff --git a/superset-frontend/src/features/annotations/AnnotationModal.tsx b/superset-frontend/src/features/annotations/AnnotationModal.tsx index fcb105af50a..e4c4488986f 100644 --- a/superset-frontend/src/features/annotations/AnnotationModal.tsx +++ b/superset-frontend/src/features/annotations/AnnotationModal.tsx @@ -21,8 +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 { extendedDayjs } from 'src/utils/dates'; import Icons from 'src/components/Icons'; import Modal from 'src/components/Modal'; import { StyledIcon } from 'src/views/CRUD/utils'; @@ -199,18 +198,23 @@ const AnnotationModal: FunctionComponent = ({ setCurrentAnnotation(data); }; - const onDateChange = (value: any, dateString: Array) => { + const onDateChange = (dates: any, dateString: Array) => { + if (!dates?.[0] || !dates?.[1]) { + const data = { + ...currentAnnotation, + start_dttm: '', + end_dttm: '', + short_descr: currentAnnotation?.short_descr ?? '', + }; + setCurrentAnnotation(data); + return; + } + const data = { ...currentAnnotation, - end_dttm: - currentAnnotation && dateString[1].length - ? moment(dateString[1]).format('YYYY-MM-DD HH:mm') - : '', - short_descr: currentAnnotation ? currentAnnotation.short_descr : '', - start_dttm: - currentAnnotation && dateString[0].length - ? moment(dateString[0]).format('YYYY-MM-DD HH:mm') - : '', + start_dttm: dates[0].format('YYYY-MM-DD HH:mm'), + end_dttm: dates[1].format('YYYY-MM-DD HH:mm'), + short_descr: currentAnnotation?.short_descr ?? '', }; setCurrentAnnotation(data); }; @@ -305,15 +309,15 @@ const AnnotationModal: FunctionComponent = ({ import('antd-v5/locale/en_US'), + fr: () => import('antd-v5/locale/fr_FR'), + es: () => import('antd-v5/locale/es_ES'), + it: () => import('antd-v5/locale/it_IT'), + zh: () => import('antd-v5/locale/zh_CN'), + ja: () => import('antd-v5/locale/ja_JP'), + de: () => import('antd-v5/locale/de_DE'), + pt: () => import('antd-v5/locale/pt_PT'), + pt_BR: () => import('antd-v5/locale/pt_BR'), + ru: () => import('antd-v5/locale/ru_RU'), + ko: () => import('antd-v5/locale/ko_KR'), + sk: () => import('antd-v5/locale/sk_SK'), + sl: () => import('antd-v5/locale/sl_SI'), + nl: () => import('antd-v5/locale/nl_NL'), +}; + +export const useLocale = (): Locale | undefined | null => { + const [datePickerLocale, setDatePickerLocale] = useState< + Locale | undefined | null + >(null); + + // Retrieve the locale from Redux store + const localFromFlaskBabel = useSelector( + (state: ExplorePageState) => state?.common?.locale, + ); + + useEffect(() => { + if (datePickerLocale === null) { + if (localFromFlaskBabel && LOCALE_MAPPING[localFromFlaskBabel]) { + LOCALE_MAPPING[localFromFlaskBabel]() + .then((locale: { default: Locale }) => { + setDatePickerLocale(locale.default); + dayjs.locale(localFromFlaskBabel); + }) + .catch(() => setDatePickerLocale(undefined)); + } else { + setDatePickerLocale(undefined); + } + } + }, [datePickerLocale, localFromFlaskBabel]); + + return datePickerLocale; +}; diff --git a/superset-frontend/src/pages/QueryHistoryList/index.tsx b/superset-frontend/src/pages/QueryHistoryList/index.tsx index 05263b1207d..a5967118bbb 100644 --- a/superset-frontend/src/pages/QueryHistoryList/index.tsx +++ b/superset-frontend/src/pages/QueryHistoryList/index.tsx @@ -214,8 +214,8 @@ function QueryList({ addDangerToast }: QueryListProps) { original: { start_time }, }, }: any) => { - const startMoment = extendedDayjs.utc(start_time).local(); - const formattedStartTimeData = startMoment + const start = extendedDayjs.utc(start_time).local(); + const formattedStartTimeData = start .format(DATETIME_WITH_TIME_ZONE) .split(' '); diff --git a/superset-frontend/src/theme/index.ts b/superset-frontend/src/theme/index.ts index efba95cee5d..7b61bfdeca0 100644 --- a/superset-frontend/src/theme/index.ts +++ b/superset-frontend/src/theme/index.ts @@ -117,6 +117,11 @@ const baseConfig: ThemeConfig = { fontWeightStrong: supersetTheme.typography.weights.medium, colorBgContainer: supersetTheme.colors.grayscale.light4, }, + DatePicker: { + colorBgContainer: supersetTheme.colors.grayscale.light5, + colorBgElevated: supersetTheme.colors.grayscale.light5, + borderRadiusSM: supersetTheme.gridUnit / 2, + }, Divider: { colorSplit: supersetTheme.colors.grayscale.light3, }, diff --git a/superset-frontend/src/utils/common.js b/superset-frontend/src/utils/common.js index e9418f9d31d..4ceceb0b412 100644 --- a/superset-frontend/src/utils/common.js +++ b/superset-frontend/src/utils/common.js @@ -30,7 +30,7 @@ export const NULL_STRING = ''; export const TRUE_STRING = 'TRUE'; export const FALSE_STRING = 'FALSE'; -// moment time format strings +// dayjs time format strings export const SHORT_DATE = 'MMM D, YYYY'; export const SHORT_TIME = 'h:m a'; diff --git a/superset-frontend/src/utils/dates.ts b/superset-frontend/src/utils/dates.ts index 5cf0cb435a0..a1155dc1af9 100644 --- a/superset-frontend/src/utils/dates.ts +++ b/superset-frontend/src/utils/dates.ts @@ -16,6 +16,7 @@ * 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'; @@ -24,6 +25,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import duration from 'dayjs/plugin/duration'; import updateLocale from 'dayjs/plugin/updateLocale'; +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; dayjs.extend(utc); dayjs.extend(timezone); @@ -32,6 +34,11 @@ dayjs.extend(relativeTime); dayjs.extend(customParseFormat); dayjs.extend(duration); dayjs.extend(updateLocale); +dayjs.extend(isSameOrBefore); + +dayjs.updateLocale('en', { + invalidDate: 'Invalid date', +}); export const extendedDayjs = dayjs; diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index dcc79a900f7..83b2507636c 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -24,7 +24,6 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const CopyPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const MomentLocalesPlugin = require('moment-locales-webpack-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); const { @@ -42,24 +41,6 @@ const APP_DIR = path.resolve(__dirname, './'); // output dir const BUILD_DIR = path.resolve(__dirname, '../superset/static/assets'); const ROOT_DIR = path.resolve(__dirname, '..'); -const TRANSLATIONS_DIR = path.resolve(__dirname, '../superset/translations'); - -const getAvailableTranslationCodes = () => { - if (process.env.BUILD_TRANSLATIONS === 'true') { - const LOCALE_CODE_MAPPING = { - zh: 'zh-cn', - }; - const files = fs.readdirSync(TRANSLATIONS_DIR); - return files - .filter(file => - fs.statSync(path.join(TRANSLATIONS_DIR, file)).isDirectory(), - ) - .filter(dirName => !dirName.startsWith('__')) - .map(dirName => dirName.replace('_', '-')) - .map(dirName => LOCALE_CODE_MAPPING[dirName] || dirName); - } - return []; -}; const { mode = 'development', @@ -159,9 +140,6 @@ const plugins = [ chunks: [], filename: '500.html', }), - new MomentLocalesPlugin({ - localesToKeep: getAvailableTranslationCodes(), - }), ]; if (!process.env.CI) {