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 b086effd2..53f23f364 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 @@ -37,5 +37,6 @@ export { default as smartDateDetailedFormatter } from './formatters/smartDateDet export { default as smartDateVerboseFormatter } from './formatters/smartDateVerbose'; export { default as normalizeTimestamp } from './utils/normalizeTimestamp'; +export { default as denormalizeTimestamp } from './utils/denormalizeTimestamp'; export * from './types'; diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/utils/denormalizeTimestamp.ts b/superset-frontend/packages/superset-ui-core/src/time-format/utils/denormalizeTimestamp.ts new file mode 100644 index 000000000..3a69667ce --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/time-format/utils/denormalizeTimestamp.ts @@ -0,0 +1,28 @@ +/* + * 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 { TS_REGEX } from './normalizeTimestamp'; + +export default function normalizeTimestamp(value: string): string { + const match = value.match(TS_REGEX); + if (match) { + return `${match[1]}T${match[2]}`; + } + return value; +} diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/utils/normalizeTimestamp.ts b/superset-frontend/packages/superset-ui-core/src/time-format/utils/normalizeTimestamp.ts index 0e49aee7e..8e5201f23 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/utils/normalizeTimestamp.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/utils/normalizeTimestamp.ts @@ -17,7 +17,7 @@ * under the License. */ -const TS_REGEX = /(\d{4}-\d{2}-\d{2})[\sT](\d{2}:\d{2}:\d{2}\.?\d*).*/; +export const TS_REGEX = /(\d{4}-\d{2}-\d{2})[\sT](\d{2}:\d{2}:\d{2}\.?\d*).*/; export default function normalizeTimestamp(value: string): string { const match = value.match(TS_REGEX); diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/utils/denormalizeTimestamp.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/utils/denormalizeTimestamp.test.ts new file mode 100644 index 000000000..27838c393 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/time-format/utils/denormalizeTimestamp.test.ts @@ -0,0 +1,43 @@ +/* + * 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 denormalizeTimestamp from '../../../src/time-format/utils/denormalizeTimestamp'; + +test('denormalizeTimestamp should normalize typical timestamps', () => { + expect(denormalizeTimestamp('2023-03-11 08:26:52.695 UTC')).toEqual( + '2023-03-11T08:26:52.695', + ); + expect( + denormalizeTimestamp('2023-03-11 08:26:52.695 Europe/Helsinki'), + ).toEqual('2023-03-11T08:26:52.695'); + expect(denormalizeTimestamp('2023-03-11T08:26:52.695 UTC')).toEqual( + '2023-03-11T08:26:52.695', + ); + expect(denormalizeTimestamp('2023-03-11T08:26:52.695')).toEqual( + '2023-03-11T08:26:52.695', + ); + expect(denormalizeTimestamp('2023-03-11 08:26:52')).toEqual( + '2023-03-11T08:26:52', + ); +}); + +test('denormalizeTimestamp should return unmatched timestamps as-is', () => { + expect(denormalizeTimestamp('abcd')).toEqual('abcd'); + expect(denormalizeTimestamp('03/11/2023')).toEqual('03/11/2023'); +}); diff --git a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.jsx b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.jsx index b06488e3b..276d8eea6 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.jsx +++ b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.jsx @@ -24,6 +24,7 @@ import SouthPane from 'src/SqlLab/components/SouthPane'; import '@testing-library/jest-dom/extend-expect'; import { STATUS_OPTIONS } from 'src/SqlLab/constants'; import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures'; +import { denormalizeTimestamp } from '@superset-ui/core'; const mockedProps = { queryEditorId: defaultQueryEditor.id, @@ -61,7 +62,7 @@ const store = mockStore({ queries: { LCly_kkIN: { cached: false, - changed_on: new Date().toISOString(), + changed_on: denormalizeTimestamp(new Date().toISOString()), db: 'main', dbId: 1, id: 'LCly_kkIN', @@ -71,7 +72,7 @@ const store = mockStore({ }, lXJa7F9_r: { cached: false, - changed_on: new Date(1559238500401).toISOString(), + changed_on: denormalizeTimestamp(new Date(1559238500401).toISOString()), db: 'main', dbId: 1, id: 'lXJa7F9_r', @@ -80,7 +81,7 @@ const store = mockStore({ }, '2g2_iRFMl': { cached: false, - changed_on: new Date(1559238506925).toISOString(), + changed_on: denormalizeTimestamp(new Date(1559238506925).toISOString()), db: 'main', dbId: 1, id: '2g2_iRFMl', @@ -89,7 +90,7 @@ const store = mockStore({ }, erWdqEWPm: { cached: false, - changed_on: new Date(1559238516395).toISOString(), + changed_on: denormalizeTimestamp(new Date(1559238516395).toISOString()), db: 'main', dbId: 1, id: 'erWdqEWPm', diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts index 6fd94e78f..658c1432d 100644 --- a/superset-frontend/src/SqlLab/fixtures.ts +++ b/superset-frontend/src/SqlLab/fixtures.ts @@ -19,7 +19,12 @@ import sinon from 'sinon'; import * as actions from 'src/SqlLab/actions/sqlLab'; import { ColumnKeyTypeType } from 'src/SqlLab/components/ColumnElement'; -import { DatasourceType, QueryResponse, QueryState } from '@superset-ui/core'; +import { + DatasourceType, + denormalizeTimestamp, + QueryResponse, + QueryState, +} from '@superset-ui/core'; import { ISaveableDatasource } from 'src/SqlLab/components/SaveDatasetModal'; export const mockedActions = sinon.stub({ ...actions }); @@ -708,7 +713,7 @@ export const mockdatasets = [...new Array(3)].map((_, i) => ({ changed_by_name: 'user', kind: i === 0 ? 'virtual' : 'physical', // ensure there is 1 virtual changed_by: 'user', - changed_on: new Date().toISOString(), + changed_on: denormalizeTimestamp(new Date().toISOString()), database_name: `db ${i}`, explore_url: `/explore/?datasource_type=table&datasource_id=${i}`, id: i, diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js index 915bb3f6b..fae868df1 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { QueryState, t } from '@superset-ui/core'; +import { normalizeTimestamp, QueryState, t } from '@superset-ui/core'; import getInitialState from './getInitialState'; import * as actions from '../actions/sqlLab'; import { now } from '../../utils/dates'; @@ -614,9 +614,10 @@ export default function sqlLabReducer(state = {}, action) { (state.queries[id].state !== QueryState.STOPPED && state.queries[id].state !== QueryState.FAILED) ) { - const changedOn = Date.parse(changedQuery.changed_on); - if (changedOn > queriesLastUpdate) { - queriesLastUpdate = changedOn; + const changedOn = normalizeTimestamp(changedQuery.changed_on); + const timestamp = Date.parse(changedOn); + if (timestamp > queriesLastUpdate) { + queriesLastUpdate = timestamp; } const prevState = state.queries[id]?.state; const currentState = changedQuery.state; diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js index f1c482e67..882781369 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js @@ -18,7 +18,6 @@ */ import sqlLabReducer from 'src/SqlLab/reducers/sqlLab'; import * as actions from 'src/SqlLab/actions/sqlLab'; -import { now } from 'src/utils/dates'; import { table, initialState as mockState } from '../fixtures'; const initialState = mockState.sqlLab; @@ -273,6 +272,8 @@ describe('sqlLabReducer', () => { }); }); describe('Run Query', () => { + const DENORMALIZED_CHANGED_ON = '2023-06-26T07:53:05.439'; + const CHANGED_ON_TIMESTAMP = 1687765985439; let newState; let query; beforeEach(() => { @@ -280,7 +281,8 @@ describe('sqlLabReducer', () => { query = { id: 'abcd', progress: 0, - startDttm: now(), + changed_on: DENORMALIZED_CHANGED_ON, + startDttm: CHANGED_ON_TIMESTAMP, state: 'running', cached: false, sqlEditorId: 'dfsadfs', @@ -292,7 +294,8 @@ describe('sqlLabReducer', () => { query: { id: 'abcd', progress: 0, - startDttm: now(), + changed_on: DENORMALIZED_CHANGED_ON, + startDttm: CHANGED_ON_TIMESTAMP, state: 'running', cached: false, sqlEditorId: 'dfsadfs', @@ -328,6 +331,19 @@ describe('sqlLabReducer', () => { newState = sqlLabReducer(newState, removeQueryAction); expect(Object.keys(newState.queries)).toHaveLength(0); }); + it('should refresh queries when polling returns new results', () => { + newState = sqlLabReducer( + { + ...newState, + queries: { abcd: {} }, + }, + actions.refreshQueries({ + abcd: query, + }), + ); + expect(newState.queries.abcd.changed_on).toBe(DENORMALIZED_CHANGED_ON); + expect(newState.queriesLastUpdate).toBe(CHANGED_ON_TIMESTAMP); + }); it('should refresh queries when polling returns empty', () => { newState = sqlLabReducer(newState, actions.refreshQueries({})); });