This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch chore/ts-migration-sqllab-explore in repository https://gitbox.apache.org/repos/asf/superset.git
commit 4458fa4dcd6ec1a6da1047fb60eccee3031e2ba9 Author: Evan Rusackas <[email protected]> AuthorDate: Thu Dec 18 23:19:18 2025 -0800 chore(frontend): migrate SqlLab and explore JS/JSX files to TypeScript Migrate 40 files from JavaScript/JSX to TypeScript: SqlLab: - actions/sqlLab.ts - Redux actions with typed thunks - middlewares/persistSqlLabStateEnhancer.ts - localStorage persistence - reducers/sqlLab.ts - Reducer with SqlLabState types Explore: - store.ts - Store initialization with ExploreState types - controls.tsx - Control definitions with Datasource interface Controls (33 files): - CheckboxControl, ViewportControl, SpatialControl with full interfaces - AnnotationLayer, AnnotationTypes with proper type exports - AdhocFilter, AdhocMetric, MetricsControl conversions - SelectControl, TextAreaControl, DatasourceControl - CollectionControl, FixedOrMetricControl, TimeSeriesColumnControl 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --- .../src/SqlLab/actions/{sqlLab.js => sqlLab.ts} | 207 ++++++++++++++++----- .../middlewares/persistSqlLabStateEnhancer.js | 136 -------------- .../middlewares/persistSqlLabStateEnhancer.ts | 184 ++++++++++++++++++ .../src/SqlLab/reducers/{sqlLab.js => sqlLab.ts} | 22 ++- .../{AnnotationLayer.jsx => AnnotationLayer.tsx} | 0 .../{AnnotationTypes.js => AnnotationTypes.ts} | 33 +++- ...undsControl.test.jsx => BoundsControl.test.tsx} | 0 .../{CheckboxControl.jsx => CheckboxControl.tsx} | 46 ++--- .../CollectionControl/{index.jsx => index.tsx} | 0 .../DatasourceControl/{index.jsx => index.tsx} | 0 ...erTitle.jsx => DndColumnSelectPopoverTitle.tsx} | 0 .../{AdhocFilter.test.js => AdhocFilter.test.ts} | 0 .../AdhocFilter/{index.js => index.ts} | 0 .../AdhocFilterControl/{index.jsx => index.tsx} | 0 ...er.test.jsx => AdhocFilterEditPopover.test.tsx} | 0 .../{index.jsx => index.tsx} | 0 .../FixedOrMetricControl/{index.jsx => index.tsx} | 0 .../{AdhocMetric.test.js => AdhocMetric.test.ts} | 0 .../{AdhocMetric.js => AdhocMetric.ts} | 0 .../{index.jsx => index.tsx} | 0 ...cOption.test.jsx => AdhocMetricOption.test.tsx} | 0 ...AdhocMetricOption.jsx => AdhocMetricOption.tsx} | 0 ...on.test.jsx => FilterDefinitionOption.test.tsx} | 0 ...nitionOption.jsx => FilterDefinitionOption.tsx} | 0 ...lue.test.jsx => MetricDefinitionValue.test.tsx} | 0 ...finitionValue.jsx => MetricDefinitionValue.tsx} | 0 ...icsControl.test.jsx => MetricsControl.test.tsx} | 0 .../{MetricsControl.jsx => MetricsControl.tsx} | 0 .../{adhocMetricType.js => adhocMetricType.ts} | 0 ...lectControl.test.jsx => SelectControl.test.tsx} | 0 .../{SelectControl.jsx => SelectControl.tsx} | 0 .../{SpatialControl.jsx => SpatialControl.tsx} | 123 +++++++----- ...eaControl.test.jsx => TextAreaControl.test.tsx} | 0 .../{TextAreaControl.jsx => TextAreaControl.tsx} | 0 .../{index.jsx => index.tsx} | 0 ...rtControl.test.jsx => ViewportControl.test.tsx} | 0 .../{ViewportControl.jsx => ViewportControl.tsx} | 81 ++++---- .../src/explore/{controls.jsx => controls.tsx} | 29 ++- .../src/explore/{store.test.jsx => store.test.tsx} | 0 .../src/explore/{store.js => store.ts} | 37 +++- 40 files changed, 572 insertions(+), 326 deletions(-) diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.ts similarity index 89% rename from superset-frontend/src/SqlLab/actions/sqlLab.js rename to superset-frontend/src/SqlLab/actions/sqlLab.ts index fd23cb5753..eb8991aec8 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.ts @@ -18,6 +18,8 @@ */ import { nanoid } from 'nanoid'; import rison from 'rison'; +import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; +import type { QueryResponse, SupersetError } from '@superset-ui/core'; import { FeatureFlag, SupersetClient, @@ -38,10 +40,63 @@ import { import { LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY } from 'src/logger/LogUtils'; import getBootstrapData from 'src/utils/getBootstrapData'; import { logEvent } from 'src/logger/actions'; +import type { QueryEditor, SqlLabRootState, Table } from '../types'; import { newQueryTabName } from '../utils/newQueryTabName'; import getInitialState from '../reducers/getInitialState'; import { rehydratePersistedState } from '../utils/reduxStateToLocalStorageHelper'; +// Type definitions for SqlLab actions +export interface Query { + id: string; + dbId?: number; + sql: string; + sqlEditorId?: string | null; + sqlEditorImmutableId?: string; + tab?: string; + catalog?: string | null; + schema?: string | null; + tempTable?: string; + templateParams?: string; + queryLimit?: number; + runAsync?: boolean; + ctas?: boolean; + ctas_method?: string; + isDataPreview?: boolean; + progress?: number; + startDttm?: number; + state?: string; + cached?: boolean; + resultsKey?: string; + updateTabState?: boolean; + tableName?: string; + link?: string; + inLocalStorage?: boolean; +} + +export interface Database { + id: number; + allow_run_async: boolean; + disable_data_preview?: boolean; +} + +interface SqlLabAction { + type: string; + [key: string]: unknown; +} + +type SqlLabThunkAction<R = void> = ThunkAction< + R, + SqlLabRootState, + unknown, + SqlLabAction +>; + +type SqlLabThunkDispatch = ThunkDispatch< + SqlLabRootState, + unknown, + SqlLabAction +>; + export const RESET_STATE = 'RESET_STATE'; export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR'; export const UPDATE_QUERY_EDITOR = 'UPDATE_QUERY_EDITOR'; @@ -112,13 +167,14 @@ export const addWarningToast = addWarningToastAction; export const CtasEnum = { Table: 'TABLE', View: 'VIEW', -}; +} as const; + const ERR_MSG_CANT_LOAD_QUERY = t("The query couldn't be loaded"); // a map of SavedQuery field names to the different names used client-side, // because for now making the names consistent is too complicated // so it might as well only happen in one place -const queryClientMapping = { +const queryClientMapping: Record<string, string> = { id: 'remoteId', db_id: 'dbId', label: 'name', @@ -127,13 +183,19 @@ const queryClientMapping = { const queryServerMapping = invert(queryClientMapping); // uses a mapping like those above to convert object key names to another style -const fieldConverter = mapping => obj => - mapKeys(obj, (value, key) => (key in mapping ? mapping[key] : key)); +const fieldConverter = + (mapping: Record<string, string>) => + <T extends Record<string, unknown>>(obj: T): Record<string, unknown> => + mapKeys(obj, (_value, key) => (key in mapping ? mapping[key] : key)); export const convertQueryToServer = fieldConverter(queryServerMapping); export const convertQueryToClient = fieldConverter(queryClientMapping); -export function getUpToDateQuery(rootState, queryEditor, key) { +export function getUpToDateQuery( + rootState: SqlLabRootState, + queryEditor: QueryEditor | { id: string }, + key?: string, +): QueryEditor { const { sqlLab: { unsavedQueryEditor, queryEditors }, } = rootState; @@ -142,10 +204,12 @@ export function getUpToDateQuery(rootState, queryEditor, key) { id, ...queryEditors.find(qe => qe.id === id), ...(id === unsavedQueryEditor.id && unsavedQueryEditor), - }; + } as QueryEditor; } -export function resetState(data) { +export function resetState( + data?: Record<string, unknown>, +): SqlLabThunkAction<void> { return (dispatch, getState) => { const { common } = getState(); const initialState = getInitialState({ @@ -162,15 +226,19 @@ export function resetState(data) { }; } -export function updateQueryEditor(alterations) { +export function updateQueryEditor( + alterations: Partial<QueryEditor>, +): SqlLabAction { return { type: UPDATE_QUERY_EDITOR, alterations }; } -export function setEditorTabLastUpdate(timestamp) { +export function setEditorTabLastUpdate(timestamp: number): SqlLabAction { return { type: SET_EDITOR_TAB_LAST_UPDATE, timestamp }; } -export function scheduleQuery(query) { +export function scheduleQuery( + query: Record<string, unknown>, +): SqlLabThunkAction<Promise<void>> { return dispatch => SupersetClient.post({ endpoint: '/api/v1/saved_query/', @@ -191,7 +259,9 @@ export function scheduleQuery(query) { ); } -export function estimateQueryCost(queryEditor) { +export function estimateQueryCost( + queryEditor: QueryEditor, +): SqlLabThunkAction<Promise<unknown[]>> { return (dispatch, getState) => { const { dbId, catalog, schema, sql, selectedText, templateParams } = getUpToDateQuery(getState(), queryEditor); @@ -232,11 +302,14 @@ export function estimateQueryCost(queryEditor) { }; } -export function clearInactiveQueries(interval) { +export function clearInactiveQueries(interval: number): SqlLabAction { return { type: CLEAR_INACTIVE_QUERIES, interval }; } -export function startQuery(query, runPreviewOnly) { +export function startQuery( + query: Query, + runPreviewOnly?: boolean, +): SqlLabAction { Object.assign(query, { id: query.id ? query.id : nanoid(11), progress: 0, @@ -247,11 +320,17 @@ export function startQuery(query, runPreviewOnly) { return { type: START_QUERY, query, runPreviewOnly }; } -export function querySuccess(query, results) { +export function querySuccess( + query: Query, + results: QueryResponse, +): SqlLabAction { return { type: QUERY_SUCCESS, query, results }; } -export function logFailedQuery(query, errors) { +export function logFailedQuery( + query: Query, + errors?: SupersetError[], +): SqlLabThunkAction<void> { return function (dispatch) { const eventData = { has_err: true, @@ -259,7 +338,9 @@ export function logFailedQuery(query, errors) { ts: new Date().getTime(), }; errors?.forEach(({ error_type: errorType, message, extra }) => { - const issueCodes = extra?.issue_codes?.map(({ code }) => code) || [-1]; + const issueCodes = ( + extra as { issue_codes?: { code: number }[] } + )?.issue_codes?.map(({ code }) => code) || [-1]; dispatch( logEvent(LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY, { ...eventData, @@ -272,34 +353,48 @@ export function logFailedQuery(query, errors) { }; } -export function createQueryFailedAction(query, msg, link, errors) { +export function createQueryFailedAction( + query: Query, + msg: string, + link?: string, + errors?: SupersetError[], +): SqlLabAction { return { type: QUERY_FAILED, query, msg, link, errors }; } -export function queryFailed(query, msg, link, errors) { +export function queryFailed( + query: Query, + msg: string, + link?: string, + errors?: SupersetError[], +): SqlLabThunkAction<void> { return function (dispatch) { dispatch(logFailedQuery(query, errors)); dispatch(createQueryFailedAction(query, msg, link, errors)); }; } -export function stopQuery(query) { +export function stopQuery(query: Query): SqlLabAction { return { type: STOP_QUERY, query }; } -export function clearQueryResults(query) { +export function clearQueryResults(query: Query): SqlLabAction { return { type: CLEAR_QUERY_RESULTS, query }; } -export function removeDataPreview(table) { +export function removeDataPreview(table: Table): SqlLabAction { return { type: REMOVE_DATA_PREVIEW, table }; } -export function requestQueryResults(query) { +export function requestQueryResults(query: Query): SqlLabAction { return { type: REQUEST_QUERY_RESULTS, query }; } -export function fetchQueryResults(query, displayLimit, timeoutInMs) { +export function fetchQueryResults( + query: Query, + displayLimit?: number, + timeoutInMs?: number, +): SqlLabThunkAction<Promise<void>> { return function (dispatch, getState) { const { SQLLAB_QUERY_RESULT_TIMEOUT } = getState().common?.conf ?? {}; dispatch(requestQueryResults(query)); @@ -315,7 +410,7 @@ export function fetchQueryResults(query, displayLimit, timeoutInMs) { parseMethod: 'json-bigint', ...(timeout && { timeout, signal: controller.signal }), }) - .then(({ json }) => dispatch(querySuccess(query, json))) + .then(({ json }) => dispatch(querySuccess(query, json as QueryResponse))) .catch(response => { controller.abort(); getClientErrorObject(response).then(error => { @@ -332,7 +427,10 @@ export function fetchQueryResults(query, displayLimit, timeoutInMs) { }; } -export function runQuery(query, runPreviewOnly) { +export function runQuery( + query: Query, + runPreviewOnly?: boolean, +): SqlLabThunkAction<Promise<void>> { return function (dispatch) { dispatch(startQuery(query, runPreviewOnly)); const postPayload = { @@ -361,7 +459,7 @@ export function runQuery(query, runPreviewOnly) { }) .then(({ json }) => { if (!query.runAsync) { - dispatch(querySuccess(query, json)); + dispatch(querySuccess(query, json as QueryResponse)); } }) .catch(response => @@ -381,16 +479,17 @@ export function runQuery(query, runPreviewOnly) { } export function runQueryFromSqlEditor( - database, - queryEditor, - defaultQueryLimit, - tempTable, - ctas, - ctasMethod, -) { + database: Database | null, + queryEditor: QueryEditor, + defaultQueryLimit: number, + tempTable?: string, + ctas?: boolean, + ctasMethod?: string, +): SqlLabThunkAction<void> { return function (dispatch, getState) { const qe = getUpToDateQuery(getState(), queryEditor, queryEditor.id); - const query = { + const query: Query = { + id: nanoid(11), dbId: qe.dbId, sql: qe.selectedText || qe.sql, sqlEditorId: qe.tabViewId ?? qe.id, @@ -410,14 +509,14 @@ export function runQueryFromSqlEditor( }; } -export function reRunQuery(query) { +export function reRunQuery(query: Query): SqlLabThunkAction<void> { // run Query with a new id return function (dispatch) { dispatch(runQuery({ ...query, id: nanoid(11) })); }; } -export function postStopQuery(query) { +export function postStopQuery(query: Query): SqlLabThunkAction<Promise<void>> { return function (dispatch) { return SupersetClient.post({ endpoint: '/api/v1/query/stop', @@ -430,11 +529,17 @@ export function postStopQuery(query) { }; } -export function setDatabases(databases) { +export function setDatabases( + databases: Record<string, Database>, +): SqlLabAction { return { type: SET_DATABASES, databases }; } -function migrateTable(table, queryEditorId, dispatch) { +function migrateTable( + table: Table, + queryEditorId: string, + dispatch: SqlLabThunkDispatch, +): Promise<void> { return SupersetClient.post({ endpoint: encodeURI('/tableschemaview/'), postPayload: { table: { ...table, queryEditorId } }, @@ -459,7 +564,11 @@ function migrateTable(table, queryEditorId, dispatch) { ); } -function migrateQuery(queryId, queryEditorId, dispatch) { +function migrateQuery( + queryId: string, + queryEditorId: string, + dispatch: SqlLabThunkDispatch, +): Promise<void> { return SupersetClient.post({ endpoint: encodeURI(`/tabstateview/${queryEditorId}/migrate_query`), postPayload: { queryId }, @@ -486,7 +595,9 @@ function migrateQuery(queryId, queryEditorId, dispatch) { * stored in local storage will also be synchronized to the backend * through syncQueryEditor. */ -export function syncQueryEditor(queryEditor) { +export function syncQueryEditor( + queryEditor: QueryEditor, +): SqlLabThunkAction<Promise<void[] | void>> { return function (dispatch, getState) { const { tables, queries } = getState().sqlLab; const localStorageTables = tables.filter( @@ -513,10 +624,10 @@ export function syncQueryEditor(queryEditor) { }); return Promise.all([ ...localStorageTables.map(table => - migrateTable(table, newQueryEditor.tabViewId, dispatch), + migrateTable(table, newQueryEditor.tabViewId!, dispatch), ), ...localStorageQueries.map(query => - migrateQuery(query.id, newQueryEditor.tabViewId, dispatch), + migrateQuery(query.id, newQueryEditor.tabViewId!, dispatch), ), ]); }) @@ -533,7 +644,9 @@ export function syncQueryEditor(queryEditor) { }; } -export function addQueryEditor(queryEditor) { +export function addQueryEditor( + queryEditor: Partial<QueryEditor>, +): SqlLabAction { const newQueryEditor = { ...queryEditor, id: nanoid(11), @@ -707,7 +820,7 @@ export function fetchQueryEditor(queryEditor, displayLimit) { }; dispatch(loadQueryEditor(loadedQueryEditor)); dispatch(setTables(json.table_schemas || [])); - if (json.latest_query && json.latest_query.resultsKey) { + if (json.latest_query?.resultsKey) { dispatch(fetchQueryResults(json.latest_query, displayLimit)); } }) @@ -812,7 +925,8 @@ export function queryEditorSetTitle(queryEditor, name, id) { } export function saveQuery(query, clientId) { - const { id, ...payload } = convertQueryToServer(query); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _id, ...payload } = convertQueryToServer(query); return dispatch => SupersetClient.post({ @@ -858,7 +972,8 @@ export const addSavedQueryToTabState = }; export function updateSavedQuery(query, clientId) { - const { id, ...payload } = convertQueryToServer(query); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _id, ...payload } = convertQueryToServer(query); return dispatch => SupersetClient.put({ diff --git a/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js deleted file mode 100644 index 9fa11de0da..0000000000 --- a/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js +++ /dev/null @@ -1,136 +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 persistState from 'redux-localstorage'; -import { pickBy } from 'lodash'; -import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core'; -import { filterUnsavedQueryEditorList } from 'src/SqlLab/components/EditorAutoSync'; -import { - emptyTablePersistData, - emptyQueryResults, - clearQueryEditors, -} from '../utils/reduxStateToLocalStorageHelper'; -import { BYTES_PER_CHAR, KB_STORAGE } from '../constants'; - -const CLEAR_ENTITY_HELPERS_MAP = { - tables: emptyTablePersistData, - queries: emptyQueryResults, - queryEditors: clearQueryEditors, - unsavedQueryEditor: qe => clearQueryEditors([qe])[0], -}; - -const sqlLabPersistStateConfig = { - paths: ['sqlLab'], - config: { - slicer: paths => state => { - const subset = {}; - paths.forEach(path => { - if (isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)) { - const { - queryEditors, - editorTabLastUpdatedAt, - unsavedQueryEditor, - tables, - queries, - tabHistory, - lastUpdatedActiveTab, - destroyedQueryEditors, - } = state.sqlLab; - const unsavedQueryEditors = filterUnsavedQueryEditorList( - queryEditors, - unsavedQueryEditor, - editorTabLastUpdatedAt, - ); - const hasUnsavedActiveTabState = - tabHistory.slice(-1)[0] !== lastUpdatedActiveTab; - const hasUnsavedDeletedQueryEditors = - Object.keys(destroyedQueryEditors).length > 0; - if ( - unsavedQueryEditors.length > 0 || - hasUnsavedActiveTabState || - hasUnsavedDeletedQueryEditors - ) { - const hasFinishedMigrationFromLocalStorage = - unsavedQueryEditors.every( - ({ inLocalStorage }) => !inLocalStorage, - ); - subset.sqlLab = { - queryEditors: unsavedQueryEditors, - ...(!hasFinishedMigrationFromLocalStorage && { - tabHistory, - tables: tables.filter(table => table.inLocalStorage), - queries: pickBy( - queries, - query => query.inLocalStorage && !query.isDataPreview, - ), - }), - ...(hasUnsavedActiveTabState && { - tabHistory, - }), - destroyedQueryEditors, - }; - } - return; - } - // this line is used to remove old data from browser localStorage. - // we used to persist all redux state into localStorage, but - // it caused configurations passed from server-side got override. - // see PR 6257 for details - delete state[path].common; // eslint-disable-line no-param-reassign - if (path === 'sqlLab') { - subset[path] = Object.fromEntries( - Object.entries(state[path]).map(([key, value]) => [ - key, - CLEAR_ENTITY_HELPERS_MAP[key]?.(value) ?? value, - ]), - ); - } - }); - - const data = JSON.stringify(subset); - // 2 digit precision - const currentSize = - Math.round(((data.length * BYTES_PER_CHAR) / KB_STORAGE) * 100) / 100; - if (state.localStorageUsageInKilobytes !== currentSize) { - state.localStorageUsageInKilobytes = currentSize; // eslint-disable-line no-param-reassign - } - - return subset; - }, - merge: (initialState, persistedState = {}) => { - const result = { - ...initialState, - ...persistedState, - sqlLab: { - ...persistedState?.sqlLab, - // Overwrite initialState over persistedState for sqlLab - // since a logic in getInitialState overrides the value from persistedState - ...initialState.sqlLab, - }, - }; - return result; - }, - }, -}; - -// TODO: requires redux-localstorage > 1.0 for typescript support -/** @type {any} */ -export const persistSqlLabStateEnhancer = persistState( - sqlLabPersistStateConfig.paths, - sqlLabPersistStateConfig.config, -); diff --git a/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.ts b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.ts new file mode 100644 index 0000000000..cd0aee25ff --- /dev/null +++ b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.ts @@ -0,0 +1,184 @@ +/** + * 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 type { StoreEnhancer } from 'redux'; +import persistState from 'redux-localstorage'; +import { pickBy } from 'lodash'; +import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core'; +import { filterUnsavedQueryEditorList } from 'src/SqlLab/components/EditorAutoSync'; +import type { + SqlLabRootState, + QueryEditor, + UnsavedQueryEditor, + Table, +} from '../types'; +import { + emptyTablePersistData, + emptyQueryResults, + clearQueryEditors, +} from '../utils/reduxStateToLocalStorageHelper'; +import { BYTES_PER_CHAR, KB_STORAGE } from '../constants'; + +type SqlLabState = SqlLabRootState['sqlLab']; + +type ClearEntityHelperValue = + | Table[] + | SqlLabState['queries'] + | QueryEditor[] + | UnsavedQueryEditor; + +interface ClearEntityHelpersMap { + tables: (tables: Table[]) => ReturnType<typeof emptyTablePersistData>; + queries: ( + queries: SqlLabState['queries'], + ) => ReturnType<typeof emptyQueryResults>; + queryEditors: ( + queryEditors: QueryEditor[], + ) => ReturnType<typeof clearQueryEditors>; + unsavedQueryEditor: ( + qe: UnsavedQueryEditor, + ) => ReturnType<typeof clearQueryEditors>[number]; +} + +const CLEAR_ENTITY_HELPERS_MAP: ClearEntityHelpersMap = { + tables: emptyTablePersistData, + queries: emptyQueryResults, + queryEditors: clearQueryEditors, + unsavedQueryEditor: (qe: UnsavedQueryEditor) => + clearQueryEditors([qe as QueryEditor])[0], +}; + +interface PersistedSqlLabState { + sqlLab?: Partial<SqlLabState>; + localStorageUsageInKilobytes?: number; +} + +const sqlLabPersistStateConfig = { + paths: ['sqlLab'], + config: { + slicer: + (paths: string[]) => + (state: SqlLabRootState): PersistedSqlLabState => { + const subset: PersistedSqlLabState = {}; + paths.forEach(path => { + if (isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)) { + const { + queryEditors, + editorTabLastUpdatedAt, + unsavedQueryEditor, + tables, + queries, + tabHistory, + lastUpdatedActiveTab, + destroyedQueryEditors, + } = state.sqlLab; + const unsavedQueryEditors = filterUnsavedQueryEditorList( + queryEditors, + unsavedQueryEditor, + editorTabLastUpdatedAt, + ); + const hasUnsavedActiveTabState = + tabHistory.slice(-1)[0] !== lastUpdatedActiveTab; + const hasUnsavedDeletedQueryEditors = + Object.keys(destroyedQueryEditors).length > 0; + if ( + unsavedQueryEditors.length > 0 || + hasUnsavedActiveTabState || + hasUnsavedDeletedQueryEditors + ) { + const hasFinishedMigrationFromLocalStorage = + unsavedQueryEditors.every( + ({ inLocalStorage }) => !inLocalStorage, + ); + subset.sqlLab = { + queryEditors: unsavedQueryEditors, + ...(!hasFinishedMigrationFromLocalStorage && { + tabHistory, + tables: tables.filter(table => table.inLocalStorage), + queries: pickBy( + queries, + query => query.inLocalStorage && !query.isDataPreview, + ), + }), + ...(hasUnsavedActiveTabState && { + tabHistory, + }), + destroyedQueryEditors, + }; + } + return; + } + // this line is used to remove old data from browser localStorage. + // we used to persist all redux state into localStorage, but + // it caused configurations passed from server-side got override. + // see PR 6257 for details + const statePath = state[path as keyof SqlLabRootState]; + if ( + statePath && + typeof statePath === 'object' && + 'common' in statePath + ) { + delete (statePath as Record<string, unknown>).common; // eslint-disable-line no-param-reassign + } + if (path === 'sqlLab') { + subset[path] = Object.fromEntries( + Object.entries(state[path]).map(([key, value]) => { + const helper = CLEAR_ENTITY_HELPERS_MAP[ + key as keyof ClearEntityHelpersMap + ] as ((val: ClearEntityHelperValue) => unknown) | undefined; + return [ + key, + helper?.(value as ClearEntityHelperValue) ?? value, + ]; + }), + ); + } + }); + + const data = JSON.stringify(subset); + // 2 digit precision + const currentSize = + Math.round(((data.length * BYTES_PER_CHAR) / KB_STORAGE) * 100) / 100; + if (state.localStorageUsageInKilobytes !== currentSize) { + state.localStorageUsageInKilobytes = currentSize; // eslint-disable-line no-param-reassign + } + + return subset; + }, + merge: ( + initialState: SqlLabRootState, + persistedState: PersistedSqlLabState = {}, + ) => ({ + ...initialState, + ...persistedState, + sqlLab: { + ...persistedState?.sqlLab, + // Overwrite initialState over persistedState for sqlLab + // since a logic in getInitialState overrides the value from persistedState + ...initialState.sqlLab, + }, + }), + }, +}; + +// redux-localstorage doesn't have TypeScript definitions +// The library returns a StoreEnhancer that persists specified paths to localStorage +export const persistSqlLabStateEnhancer = persistState( + sqlLabPersistStateConfig.paths, + sqlLabPersistStateConfig.config, +) as StoreEnhancer; diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.ts similarity index 97% rename from superset-frontend/src/SqlLab/reducers/sqlLab.js rename to superset-frontend/src/SqlLab/reducers/sqlLab.ts index 3469036da4..8a533c4140 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.ts @@ -20,6 +20,7 @@ import { normalizeTimestamp, QueryState, t } from '@superset-ui/core'; import { isEqual, omit } from 'lodash'; import { shallowEqual } from 'react-redux'; import { now } from '@superset-ui/core/utils/dates'; +import type { SqlLabRootState, QueryEditor } from '../types'; import * as actions from '../actions/sqlLab'; import { addToObject, @@ -31,7 +32,19 @@ import { extendArr, } from '../../reduxUtils'; -function alterUnsavedQueryEditorState(state, updatedState, id, silent = false) { +type SqlLabState = SqlLabRootState['sqlLab']; + +interface SqlLabAction { + type: string; + [key: string]: unknown; +} + +function alterUnsavedQueryEditorState( + state: SqlLabState, + updatedState: Partial<QueryEditor>, + id: string, + silent = false, +): Partial<SqlLabState> { if (state.tabHistory[state.tabHistory.length - 1] !== id) { const { queryEditors } = alterInArr( state, @@ -52,8 +65,11 @@ function alterUnsavedQueryEditorState(state, updatedState, id, silent = false) { }; } -export default function sqlLabReducer(state = {}, action) { - const actionHandlers = { +export default function sqlLabReducer( + state: SqlLabState = {} as SqlLabState, + action: SqlLabAction, +): SqlLabState { + const actionHandlers: Record<string, () => SqlLabState> = { [actions.ADD_QUERY_EDITOR]() { const mergeUnsavedState = alterInArr( state, diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx rename to superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.js b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.ts similarity index 75% rename from superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.js rename to superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.ts index cdf7a161f7..d8534d8f55 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.js +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.ts @@ -18,12 +18,25 @@ */ import { t } from '@superset-ui/core'; -function extractTypes(metadata) { - return Object.keys(metadata).reduce((prev, key) => { - const result = prev; - result[key] = key; - return result; - }, {}); +interface Annotation { + sourceType?: string; + timeColumn?: string; + intervalEndColumn?: string; + titleColumn?: string; + descriptionColumns?: string[]; +} + +function extractTypes<T extends Record<string, { value: string }>>( + metadata: T, +): Record<keyof T, string> { + return Object.keys(metadata).reduce( + (prev, key) => { + const result = prev; + result[key as keyof T] = key; + return result; + }, + {} as Record<keyof T, string>, + ); } export const ANNOTATION_TYPES_METADATA = { @@ -62,7 +75,9 @@ export const ANNOTATION_SOURCE_TYPES = extractTypes( ANNOTATION_SOURCE_TYPES_METADATA, ); -export function requiresQuery(annotationSourceType) { +export function requiresQuery( + annotationSourceType: string | undefined, +): boolean { return !!annotationSourceType; } @@ -71,9 +86,9 @@ const NATIVE_COLUMN_NAMES = { intervalEndColumn: 'end_dttm', titleColumn: 'short_descr', descriptionColumns: ['long_descr'], -}; +} as const; -export function applyNativeColumns(annotation) { +export function applyNativeColumns(annotation: Annotation): Annotation { if (annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { return { ...annotation, ...NATIVE_COLUMN_NAMES }; } diff --git a/superset-frontend/src/explore/components/controls/BoundsControl.test.jsx b/superset-frontend/src/explore/components/controls/BoundsControl.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/BoundsControl.test.jsx rename to superset-frontend/src/explore/components/controls/BoundsControl.test.tsx diff --git a/superset-frontend/src/explore/components/controls/CheckboxControl.jsx b/superset-frontend/src/explore/components/controls/CheckboxControl.tsx similarity index 70% rename from superset-frontend/src/explore/components/controls/CheckboxControl.jsx rename to superset-frontend/src/explore/components/controls/CheckboxControl.tsx index dd551dd996..b3e5b3cb66 100644 --- a/superset-frontend/src/explore/components/controls/CheckboxControl.jsx +++ b/superset-frontend/src/explore/components/controls/CheckboxControl.tsx @@ -16,22 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, type ReactNode } from 'react'; import { styled, css } from '@apache-superset/core/ui'; import { Checkbox } from '@superset-ui/core/components'; import ControlHeader from '../ControlHeader'; -const propTypes = { - value: PropTypes.bool, - label: PropTypes.string, - onChange: PropTypes.func, -}; - -const defaultProps = { - value: false, - onChange: () => {}, -}; +interface CheckboxControlProps { + value?: boolean; + label?: string; + onChange?: (value: boolean) => void; +} const CheckBoxControlWrapper = styled.div` ${({ theme }) => css` @@ -47,28 +41,28 @@ const CheckBoxControlWrapper = styled.div` `} `; -export default class CheckboxControl extends Component { - onChange() { - this.props.onChange(!this.props.value); - } +export default class CheckboxControl extends Component<CheckboxControlProps> { + static defaultProps = { + value: false, + onChange: () => {}, + }; + + onChange = (): void => { + this.props.onChange?.(!this.props.value); + }; - renderCheckbox() { - return ( - <Checkbox - onChange={this.onChange.bind(this)} - checked={!!this.props.value} - /> - ); + renderCheckbox(): ReactNode { + return <Checkbox onChange={this.onChange} checked={!!this.props.value} />; } - render() { + render(): ReactNode { if (this.props.label) { return ( <CheckBoxControlWrapper> <ControlHeader {...this.props} leftNode={this.renderCheckbox()} - onClick={this.onChange.bind(this)} + onClick={this.onChange} /> </CheckBoxControlWrapper> ); @@ -76,5 +70,3 @@ export default class CheckboxControl extends Component { return this.renderCheckbox(); } } -CheckboxControl.propTypes = propTypes; -CheckboxControl.defaultProps = defaultProps; diff --git a/superset-frontend/src/explore/components/controls/CollectionControl/index.jsx b/superset-frontend/src/explore/components/controls/CollectionControl/index.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/CollectionControl/index.jsx rename to superset-frontend/src/explore/components/controls/CollectionControl/index.tsx diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx rename to superset-frontend/src/explore/components/controls/DatasourceControl/index.tsx diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx rename to superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/AdhocFilter.test.js b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/AdhocFilter.test.ts similarity index 100% rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/AdhocFilter.test.js rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/AdhocFilter.test.ts diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.ts similarity index 100% rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.ts diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.jsx rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.tsx diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.tsx diff --git a/superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.jsx b/superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.jsx rename to superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.js b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.ts similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.js rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.ts diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.js b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.ts similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.js rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.ts diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.jsx b/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx rename to superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.tsx diff --git a/superset-frontend/src/explore/components/controls/MetricControl/adhocMetricType.js b/superset-frontend/src/explore/components/controls/MetricControl/adhocMetricType.ts similarity index 100% rename from superset-frontend/src/explore/components/controls/MetricControl/adhocMetricType.js rename to superset-frontend/src/explore/components/controls/MetricControl/adhocMetricType.ts diff --git a/superset-frontend/src/explore/components/controls/SelectControl.test.jsx b/superset-frontend/src/explore/components/controls/SelectControl.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/SelectControl.test.jsx rename to superset-frontend/src/explore/components/controls/SelectControl.test.tsx diff --git a/superset-frontend/src/explore/components/controls/SelectControl.jsx b/superset-frontend/src/explore/components/controls/SelectControl.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/SelectControl.jsx rename to superset-frontend/src/explore/components/controls/SelectControl.tsx diff --git a/superset-frontend/src/explore/components/controls/SpatialControl.jsx b/superset-frontend/src/explore/components/controls/SpatialControl.tsx similarity index 72% rename from superset-frontend/src/explore/components/controls/SpatialControl.jsx rename to superset-frontend/src/explore/components/controls/SpatialControl.tsx index 52575341cb..3ec41e37ff 100644 --- a/superset-frontend/src/explore/components/controls/SpatialControl.jsx +++ b/superset-frontend/src/explore/components/controls/SpatialControl.tsx @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, type ReactNode } from 'react'; import { Row, Col, @@ -35,27 +34,54 @@ const spatialTypes = { latlong: 'latlong', delimited: 'delimited', geohash: 'geohash', -}; - -const propTypes = { - onChange: PropTypes.func, - value: PropTypes.object, - animation: PropTypes.bool, - choices: PropTypes.array, -}; - -const defaultProps = { - onChange: () => {}, - animation: true, - choices: [], -}; - -export default class SpatialControl extends Component { - constructor(props) { +} as const; + +type SpatialType = (typeof spatialTypes)[keyof typeof spatialTypes]; + +interface SpatialValue { + type: SpatialType; + latCol?: string; + lonCol?: string; + lonlatCol?: string; + delimiter?: string; + reverseCheckbox?: boolean; + geohashCol?: string; +} + +interface SpatialControlProps { + onChange?: (value: SpatialValue, errors: string[]) => void; + value?: SpatialValue; + animation?: boolean; + choices?: [string, string][]; +} + +interface SpatialControlState { + type: SpatialType; + delimiter: string; + latCol: string | undefined; + lonCol: string | undefined; + lonlatCol: string | undefined; + reverseCheckbox: boolean; + geohashCol: string | undefined; + value: SpatialValue | null; + errors: string[]; +} + +export default class SpatialControl extends Component< + SpatialControlProps, + SpatialControlState +> { + static defaultProps = { + onChange: () => {}, + animation: true, + choices: [], + }; + + constructor(props: SpatialControlProps) { super(props); - const v = props.value || {}; - let defaultCol; - if (props.choices.length > 0) { + const v = props.value || ({} as SpatialValue); + let defaultCol: string | undefined; + if (props.choices && props.choices.length > 0) { defaultCol = props.choices[0][0]; } this.state = { @@ -69,19 +95,16 @@ export default class SpatialControl extends Component { value: null, errors: [], }; - this.toggleCheckbox = this.toggleCheckbox.bind(this); - this.onChange = this.onChange.bind(this); - this.renderReverseCheckbox = this.renderReverseCheckbox.bind(this); } - componentDidMount() { + componentDidMount(): void { this.onChange(); } - onChange() { + onChange = (): void => { const { type } = this.state; - const value = { type }; - const errors = []; + const value: SpatialValue = { type }; + const errors: string[] = []; const errMsg = t('Invalid lat/long configuration.'); if (type === spatialTypes.latlong) { value.latCol = this.state.latCol; @@ -104,21 +127,21 @@ export default class SpatialControl extends Component { } } this.setState({ value, errors }); - this.props.onChange(value, errors); - } + this.props.onChange?.(value, errors); + }; - setType(type) { + setType = (type: SpatialType): void => { this.setState({ type }, this.onChange); - } + }; - toggleCheckbox() { + toggleCheckbox = (): void => { this.setState( prevState => ({ reverseCheckbox: !prevState.reverseCheckbox }), this.onChange, ); - } + }; - renderLabelContent() { + renderLabelContent(): string | null { if (this.state.errors.length > 0) { return 'N/A'; } @@ -134,25 +157,28 @@ export default class SpatialControl extends Component { return null; } - renderSelect(name, type) { + renderSelect(name: keyof SpatialControlState, type: SpatialType): ReactNode { return ( <SelectControl ariaLabel={name} name={name} choices={this.props.choices} - value={this.state[name]} + value={this.state[name] as string} clearable={false} onFocus={() => { this.setType(type); }} - onChange={value => { - this.setState({ [name]: value }, this.onChange); + onChange={(value: string) => { + this.setState( + { [name]: value } as unknown as SpatialControlState, + this.onChange, + ); }} /> ); } - renderReverseCheckbox() { + renderReverseCheckbox(): ReactNode { return ( <span> {t('Reverse lat/long ')} @@ -164,13 +190,13 @@ export default class SpatialControl extends Component { ); } - renderPopoverContent() { + renderPopoverContent(): ReactNode { return ( <div style={{ width: '300px' }}> <PopoverSection title={t('Longitude & Latitude columns')} isSelected={this.state.type === spatialTypes.latlong} - onSelect={this.setType.bind(this, spatialTypes.latlong)} + onSelect={() => this.setType(spatialTypes.latlong)} > <Row gutter={16}> <Col xs={24} md={12}> @@ -190,7 +216,7 @@ export default class SpatialControl extends Component { 'Python library for more details', )} isSelected={this.state.type === spatialTypes.delimited} - onSelect={this.setType.bind(this, spatialTypes.delimited)} + onSelect={() => this.setType(spatialTypes.delimited)} > <Row gutter={16}> <Col xs={24} md={12}> @@ -205,7 +231,7 @@ export default class SpatialControl extends Component { <PopoverSection title={t('Geohash')} isSelected={this.state.type === spatialTypes.geohash} - onSelect={this.setType.bind(this, spatialTypes.geohash)} + onSelect={() => this.setType(spatialTypes.geohash)} > <Row gutter={16}> <Col xs={24} md={12}> @@ -221,13 +247,13 @@ export default class SpatialControl extends Component { ); } - render() { + render(): ReactNode { return ( <div> <ControlHeader {...this.props} /> <Popover content={this.renderPopoverContent()} - placement="topLeft" // so that popover doesn't move when label changes + placement="topLeft" trigger="click" > <Label className="pointer">{this.renderLabelContent()}</Label> @@ -236,6 +262,3 @@ export default class SpatialControl extends Component { ); } } - -SpatialControl.propTypes = propTypes; -SpatialControl.defaultProps = defaultProps; diff --git a/superset-frontend/src/explore/components/controls/TextAreaControl.test.jsx b/superset-frontend/src/explore/components/controls/TextAreaControl.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/TextAreaControl.test.jsx rename to superset-frontend/src/explore/components/controls/TextAreaControl.test.tsx diff --git a/superset-frontend/src/explore/components/controls/TextAreaControl.jsx b/superset-frontend/src/explore/components/controls/TextAreaControl.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/TextAreaControl.jsx rename to superset-frontend/src/explore/components/controls/TextAreaControl.tsx diff --git a/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.jsx b/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.jsx rename to superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.tsx diff --git a/superset-frontend/src/explore/components/controls/ViewportControl.test.jsx b/superset-frontend/src/explore/components/controls/ViewportControl.test.tsx similarity index 100% rename from superset-frontend/src/explore/components/controls/ViewportControl.test.jsx rename to superset-frontend/src/explore/components/controls/ViewportControl.test.tsx diff --git a/superset-frontend/src/explore/components/controls/ViewportControl.jsx b/superset-frontend/src/explore/components/controls/ViewportControl.tsx similarity index 63% rename from superset-frontend/src/explore/components/controls/ViewportControl.jsx rename to superset-frontend/src/explore/components/controls/ViewportControl.tsx index daf82d14f0..91ca0f1727 100644 --- a/superset-frontend/src/explore/components/controls/ViewportControl.jsx +++ b/superset-frontend/src/explore/components/controls/ViewportControl.tsx @@ -16,16 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -import { Component } from 'react'; +import { Component, type ReactNode } from 'react'; import { t } from '@superset-ui/core'; -import PropTypes from 'prop-types'; import { Popover, FormLabel, Label } from '@superset-ui/core/components'; import { decimal2sexagesimal } from 'geolib'; import TextControl from './TextControl'; import ControlHeader from '../ControlHeader'; -export const DEFAULT_VIEWPORT = { +export interface Viewport { + longitude: number; + latitude: number; + zoom: number; + bearing: number; + pitch: number; +} + +export const DEFAULT_VIEWPORT: Viewport = { longitude: 6.85236157047845, latitude: 31.222656842808707, zoom: 1, @@ -33,54 +40,49 @@ export const DEFAULT_VIEWPORT = { pitch: 0, }; -const PARAMS = ['longitude', 'latitude', 'zoom', 'bearing', 'pitch']; +const PARAMS: (keyof Viewport)[] = [ + 'longitude', + 'latitude', + 'zoom', + 'bearing', + 'pitch', +]; -const propTypes = { - onChange: PropTypes.func, - value: PropTypes.shape({ - longitude: PropTypes.number, - latitude: PropTypes.number, - zoom: PropTypes.number, - bearing: PropTypes.number, - pitch: PropTypes.number, - }), - default: PropTypes.object, - name: PropTypes.string.isRequired, -}; - -const defaultProps = { - onChange: () => {}, - default: { type: 'fix', value: 5 }, - value: DEFAULT_VIEWPORT, -}; +interface ViewportControlProps { + onChange?: (value: Viewport) => void; + value?: Viewport; + default?: Record<string, unknown>; + name: string; +} -export default class ViewportControl extends Component { - constructor(props) { - super(props); - this.onChange = this.onChange.bind(this); - } +export default class ViewportControl extends Component<ViewportControlProps> { + static defaultProps = { + onChange: () => {}, + default: { type: 'fix', value: 5 }, + value: DEFAULT_VIEWPORT, + }; - onChange(ctrl, value) { - this.props.onChange({ - ...this.props.value, + onChange = (ctrl: keyof Viewport, value: number): void => { + this.props.onChange?.({ + ...this.props.value!, [ctrl]: value, }); - } + }; - renderTextControl(ctrl) { + renderTextControl(ctrl: keyof Viewport): ReactNode { return ( <div key={ctrl}> <FormLabel>{ctrl}</FormLabel> <TextControl - value={this.props.value[ctrl]} - onChange={this.onChange.bind(this, ctrl)} + value={this.props.value?.[ctrl]} + onChange={(value: number) => this.onChange(ctrl, value)} isFloat /> </div> ); } - renderPopover() { + renderPopover(): ReactNode { return ( <div id={`filter-popover-${this.props.name}`}> {PARAMS.map(ctrl => this.renderTextControl(ctrl))} @@ -88,8 +90,8 @@ export default class ViewportControl extends Component { ); } - renderLabel() { - if (this.props.value.longitude && this.props.value.latitude) { + renderLabel(): string { + if (this.props.value?.longitude && this.props.value?.latitude) { return `${decimal2sexagesimal( this.props.value.longitude, )} | ${decimal2sexagesimal(this.props.value.latitude)}`; @@ -97,7 +99,7 @@ export default class ViewportControl extends Component { return 'N/A'; } - render() { + render(): ReactNode { return ( <div> <ControlHeader {...this.props} /> @@ -114,6 +116,3 @@ export default class ViewportControl extends Component { ); } } - -ViewportControl.propTypes = propTypes; -ViewportControl.defaultProps = defaultProps; diff --git a/superset-frontend/src/explore/controls.jsx b/superset-frontend/src/explore/controls.tsx similarity index 96% rename from superset-frontend/src/explore/controls.jsx rename to superset-frontend/src/explore/controls.tsx index 6deea0274a..0c85b83301 100644 --- a/superset-frontend/src/explore/controls.jsx +++ b/superset-frontend/src/explore/controls.tsx @@ -56,6 +56,7 @@ * in tandem with `controlPanels/index.js` that defines how controls are composed into sections for * each and every visualization type. */ +import type { Column } from '@superset-ui/core'; import { t, getCategoricalSchemeRegistry, @@ -67,6 +68,14 @@ import { formatSelectOptions } from 'src/explore/exploreUtils'; import { TIME_FILTER_LABELS } from './constants'; import { StyledColumnOption } from './components/optionRenderers'; +interface Datasource { + columns: Column[]; + metrics: unknown[]; + granularity_sqla?: Column[]; + main_dttm_col?: string; + time_grain_sqla?: unknown[]; +} + const categoricalSchemeRegistry = getCategoricalSchemeRegistry(); const sequentialSchemeRegistry = getSequentialSchemeRegistry(); @@ -137,7 +146,7 @@ const groupByControl = { const newState = {}; if (state.datasource) { newState.options = state.datasource.columns.filter(c => c.groupby); - if (control && control.includeTime) { + if (control?.includeTime) { newState.options.push(timeColumnOption); } } @@ -167,10 +176,18 @@ const metric = { description: t('Metric'), }; -export function columnChoices(datasource) { - if (datasource && datasource.columns) { +export function columnChoices( + datasource: Datasource | null | undefined, +): [string, string][] { + if (datasource?.columns) { return datasource.columns - .map(col => [col.column_name, col.verbose_name || col.column_name]) + .map( + col => + [col.column_name, col.verbose_name || col.column_name] as [ + string, + string, + ], + ) .sort((opt1, opt2) => opt1[1].toLowerCase() > opt2[1].toLowerCase() ? 1 : -1, ); @@ -425,9 +442,7 @@ export const controls = { description: D3_FORMAT_DOCS, mapStateToProps: state => { const showWarning = - state.controls && - state.controls.comparison_type && - state.controls.comparison_type.value === 'percentage'; + state.controls?.comparison_type?.value === 'percentage'; return { warning: showWarning ? t( diff --git a/superset-frontend/src/explore/store.test.jsx b/superset-frontend/src/explore/store.test.tsx similarity index 100% rename from superset-frontend/src/explore/store.test.jsx rename to superset-frontend/src/explore/store.test.tsx diff --git a/superset-frontend/src/explore/store.js b/superset-frontend/src/explore/store.ts similarity index 77% rename from superset-frontend/src/explore/store.js rename to superset-frontend/src/explore/store.ts index 9c2fa52127..d7fbf359c5 100644 --- a/superset-frontend/src/explore/store.js +++ b/superset-frontend/src/explore/store.ts @@ -18,10 +18,28 @@ */ /* eslint camelcase: 0 */ import { getChartControlPanelRegistry, VizType } from '@superset-ui/core'; +import type { QueryFormData } from '@superset-ui/core'; import { getAllControlsState, getFormDataFromControls } from './controlUtils'; import { controls } from './controls'; -function handleDeprecatedControls(formData) { +interface ExploreState { + common?: { + conf: { + DEFAULT_VIZ_TYPE?: string; + }; + }; + datasource: { + type: string; + }; +} + +type FormData = QueryFormData & { + y_axis_zero?: boolean; + y_axis_bounds?: [number | null, number | null]; + datasource?: string; +}; + +function handleDeprecatedControls(formData: FormData): void { // Reaffectation / handling of deprecated controls /* eslint-disable no-param-reassign */ @@ -31,7 +49,10 @@ function handleDeprecatedControls(formData) { } } -export function getControlsState(state, inputFormData) { +export function getControlsState( + state: ExploreState, + inputFormData: FormData, +): Record<string, unknown> { /* * Gets a new controls object to put in the state. The controls object * is similar to the configuration control with only the controls @@ -60,8 +81,10 @@ export function getControlsState(state, inputFormData) { return controlsState; } -export function applyDefaultFormData(inputFormData) { - const datasourceType = inputFormData.datasource.split('__')[1]; +export function applyDefaultFormData( + inputFormData: FormData, +): Record<string, unknown> { + const datasourceType = inputFormData.datasource?.split('__')[1] ?? ''; const vizType = inputFormData.viz_type; const controlsState = getAllControlsState( vizType, @@ -71,14 +94,14 @@ export function applyDefaultFormData(inputFormData) { ); const controlFormData = getFormDataFromControls(controlsState); - const formData = {}; + const formData: Record<string, unknown> = {}; Object.keys(controlsState) .concat(Object.keys(inputFormData)) .forEach(controlName => { - if (inputFormData[controlName] === undefined) { + if (inputFormData[controlName as keyof FormData] === undefined) { formData[controlName] = controlFormData[controlName]; } else { - formData[controlName] = inputFormData[controlName]; + formData[controlName] = inputFormData[controlName as keyof FormData]; } });
