/** * 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, );