diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js index 24e4886ecd..316102c5c2 100644 --- a/superset-frontend/jest.config.js +++ b/superset-frontend/jest.config.js @@ -17,6 +17,9 @@ * under the License. */ +// timezone for unit tests +process.env.TZ = 'America/New_York'; + module.exports = { testRegex: '\\/superset-frontend\\/(spec|src|plugins|packages|tools)\\/.*(_spec|\\.test)\\.[jt]sx?$', diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.test.ts b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.test.ts new file mode 100644 index 0000000000..6e4f07df4b --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.test.ts @@ -0,0 +1,63 @@ +/* + * 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 finestTemporalGrain from './finestTemporalGrain'; + +test('finestTemporalGrain', () => { + const monthFormatter = finestTemporalGrain([ + new Date('2003-01-01 00:00:00Z').getTime(), + new Date('2003-02-01 00:00:00Z').getTime(), + ]); + expect(monthFormatter(new Date('2003-01-01 00:00:00Z').getTime())).toBe( + '2003-01-01', + ); + expect(monthFormatter(new Date('2003-02-01 00:00:00Z').getTime())).toBe( + '2003-02-01', + ); + + const yearFormatter = finestTemporalGrain([ + new Date('2003-01-01 00:00:00Z').getTime(), + new Date('2004-01-01 00:00:00Z').getTime(), + ]); + expect(yearFormatter(new Date('2003-01-01 00:00:00Z').getTime())).toBe( + '2003', + ); + expect(yearFormatter(new Date('2004-01-01 00:00:00Z').getTime())).toBe( + '2004', + ); + + const milliSecondFormatter = finestTemporalGrain([ + new Date('2003-01-01 00:00:00Z').getTime(), + new Date('2003-04-05 06:07:08.123Z').getTime(), + ]); + expect(milliSecondFormatter(new Date('2003-01-01 00:00:00Z').getTime())).toBe( + '2003-01-01 00:00:00.000', + ); + + const localTimeFormatter = finestTemporalGrain( + [ + new Date('2003-01-01 00:00:00Z').getTime(), + new Date('2003-02-01 00:00:00Z').getTime(), + ], + true, + ); + expect(localTimeFormatter(new Date('2003-01-01 00:00:00Z').getTime())).toBe( + '2002-12-31 19:00', + ); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.ts b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.ts new file mode 100644 index 0000000000..c03b7ec159 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/time-format/formatters/finestTemporalGrain.ts @@ -0,0 +1,80 @@ +/* + * 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 { utcFormat, timeFormat } from 'd3-time-format'; +import { utcUtils, localTimeUtils } from '../utils/d3Time'; +import TimeFormatter from '../TimeFormatter'; + +/* + * A formatter that examines all the values, and uses the finest temporal grain. + */ +export default function finestTemporalGrain( + values: any[], + useLocalTime = false, +) { + const format = useLocalTime ? timeFormat : utcFormat; + + const formatMillisecond = format('%Y-%m-%d %H:%M:%S.%L'); + const formatSecond = format('%Y-%m-%d %H:%M:%S'); + const formatMinute = format('%Y-%m-%d %H:%M'); + const formatHour = format('%Y-%m-%d %H:%M'); + const formatDay = format('%Y-%m-%d'); + const formatMonth = format('%Y-%m-%d'); + const formatYear = format('%Y'); + + const { + hasMillisecond, + hasSecond, + hasMinute, + hasHour, + isNotFirstDayOfMonth, + isNotFirstMonth, + } = useLocalTime ? localTimeUtils : utcUtils; + + let formatFunc = formatYear; + values.forEach((value: any) => { + if (formatFunc === formatYear && isNotFirstMonth(value)) { + formatFunc = formatMonth; + } + if (formatFunc === formatMonth && isNotFirstDayOfMonth(value)) { + formatFunc = formatDay; + } + if (formatFunc === formatDay && hasHour(value)) { + formatFunc = formatHour; + } + if (formatFunc === formatHour && hasMinute(value)) { + formatFunc = formatMinute; + } + if (formatFunc === formatMinute && hasSecond(value)) { + formatFunc = formatSecond; + } + if (formatFunc === formatSecond && hasMillisecond(value)) { + formatFunc = formatMillisecond; + } + }); + + return new TimeFormatter({ + description: + 'Use the finest grain in an array of dates to format all dates in the array', + formatFunc, + id: 'finest_temporal_grain', + label: 'Format temporal columns with the finest grain', + useLocalTime, + }); +} diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/index.ts b/superset-frontend/packages/superset-ui-core/src/time-format/index.ts index 53f23f3643..b0d95c1433 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/index.ts @@ -35,6 +35,7 @@ export { default as createMultiFormatter } from './factories/createMultiFormatte export { default as smartDateFormatter } from './formatters/smartDate'; export { default as smartDateDetailedFormatter } from './formatters/smartDateDetailed'; export { default as smartDateVerboseFormatter } from './formatters/smartDateVerbose'; +export { default as finestTemporalGrainFormatter } from './formatters/finestTemporalGrain'; export { default as normalizeTimestamp } from './utils/normalizeTimestamp'; export { default as denormalizeTimestamp } from './utils/denormalizeTimestamp'; diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx index 2c5d919188..bef70e68f3 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx @@ -26,7 +26,7 @@ import { GenericDataType, getColumnLabel, JsonObject, - smartDateDetailedFormatter, + finestTemporalGrainFormatter, t, tn, } from '@superset-ui/core'; @@ -117,9 +117,9 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { const labelFormatter = useMemo( () => getDataRecordFormatter({ - timeFormatter: smartDateDetailedFormatter, + timeFormatter: finestTemporalGrainFormatter(data.map(el => el.col)), }), - [], + [data], ); const updateDataMask = useCallback(