[SQL Lab] Show warning when user used up localStorage (#7572)

This commit is contained in:
Grace Guo 2019-06-07 14:27:57 -07:00 committed by GitHub
parent 883a02ae19
commit 39d67cbc59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 175 additions and 9 deletions

View File

@ -34,10 +34,10 @@ describe('SouthPane', () => {
const mockedProps = {
editorQueries: [
{ cached: false, changedOn: 1559238552333, db: 'main', dbId: 1, id: 'LCly_kkIN' },
{ cached: false, changedOn: 1559238500401, db: 'main', dbId: 1, id: 'lXJa7F9_r' },
{ cached: false, changedOn: 1559238506925, db: 'main', dbId: 1, id: '2g2_iRFMl' },
{ cached: false, changedOn: 1559238516395, db: 'main', dbId: 1, id: 'erWdqEWPm' },
{ cached: false, changedOn: Date.now(), db: 'main', dbId: 1, id: 'LCly_kkIN', startDttm: Date.now() },
{ cached: false, changedOn: 1559238500401, db: 'main', dbId: 1, id: 'lXJa7F9_r', startDttm: 1559238500401 },
{ cached: false, changedOn: 1559238506925, db: 'main', dbId: 1, id: '2g2_iRFMl', startDttm: 1559238506925 },
{ cached: false, changedOn: 1559238516395, db: 'main', dbId: 1, id: 'erWdqEWPm', startDttm: 1559238516395 },
],
latestQueryId: 'LCly_kkIN',
dataPreviewQueries: [],

View File

@ -40,7 +40,6 @@ describe('SqlEditor', () => {
queryEditor: initialState.sqlLab.queryEditors[0],
latestQuery: queries[0],
tables: [table],
queries,
getHeight: () => ('100px'),
editorQueries: [],
dataPreviewQueries: [],

View File

@ -0,0 +1,42 @@
/**
* 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 emptyQueryResults from '../../../../src/SqlLab/utils/emptyQueryResults';
import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../../../../src/SqlLab/constants';
import { queries } from '../fixtures';
describe('emptyQueryResults', () => {
const queriesObj = {};
beforeEach(() => {
queries.forEach((q) => {
queriesObj[q.id] = q;
});
});
it('should empty query.results if query.startDttm is > LOCALSTORAGE_MAX_QUERY_AGE_MS', () => {
// make sure sample data contains old query
const oldQuery = queries[0];
const { id, startDttm } = oldQuery;
expect(Date.now() - startDttm).toBeGreaterThan(LOCALSTORAGE_MAX_QUERY_AGE_MS);
expect(Object.keys(oldQuery.results)).toContain('data');
const emptiedQuery = emptyQueryResults(queriesObj);
expect(emptiedQuery[id].startDttm).toBe(startDttm);
expect(emptiedQuery[id].results).toEqual({});
});
});

View File

@ -27,6 +27,8 @@ import getInitialState from './reducers/getInitialState';
import rootReducer from './reducers/index';
import { initEnhancer } from '../reduxUtils';
import App from './components/App';
import emptyQueryResults from './utils/emptyQueryResults';
import { BYTES_PER_CHAR, KB_STORAGE } from './constants';
import setupApp from '../setup/setupApp';
import './main.less';
@ -50,8 +52,22 @@ const sqlLabPersistStateConfig = {
// it caused configurations passed from server-side got override.
// see PR 6257 for details
delete state[path].common; // eslint-disable-line no-param-reassign
subset[path] = state[path];
if (path === 'sqlLab') {
subset[path] = {
...state[path],
queries: emptyQueryResults(state[path].queries),
};
}
});
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;
},
},

View File

@ -21,11 +21,18 @@ import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import $ from 'jquery';
import { t } from '@superset-ui/translation';
import throttle from 'lodash/throttle';
import TabbedSqlEditors from './TabbedSqlEditors';
import QueryAutoRefresh from './QueryAutoRefresh';
import QuerySearch from './QuerySearch';
import ToastPresenter from '../../messageToasts/containers/ToastPresenter';
import {
LOCALSTORAGE_MAX_USAGE_KB,
LOCALSTORAGE_WARNING_THRESHOLD,
LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS,
} from '../constants';
import * as Actions from '../actions/sqlLab';
class App extends React.PureComponent {
@ -35,6 +42,12 @@ class App extends React.PureComponent {
hash: window.location.hash,
contentHeight: '0px',
};
this.showLocalStorageUsageWarning = throttle(
this.showLocalStorageUsageWarning,
LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS,
{ trailing: false },
);
}
componentDidMount() {
/* eslint-disable react/no-did-mount-set-state */
@ -42,6 +55,13 @@ class App extends React.PureComponent {
window.addEventListener('hashchange', this.onHashChanged.bind(this));
window.addEventListener('resize', this.handleResize.bind(this));
}
componentDidUpdate() {
if (this.props.localStorageUsageInKilobytes >=
LOCALSTORAGE_WARNING_THRESHOLD * LOCALSTORAGE_MAX_USAGE_KB
) {
this.showLocalStorageUsageWarning(this.props.localStorageUsageInKilobytes);
}
}
componentWillUnmount() {
window.removeEventListener('hashchange', this.onHashChanged.bind(this));
window.removeEventListener('resize', this.handleResize.bind(this));
@ -65,6 +85,15 @@ class App extends React.PureComponent {
const alertHeight = alertEl.length > 0 ? alertEl.outerHeight() : 0;
return `${window.innerHeight - headerHeight - tabsHeight - warningHeight - alertHeight}px`;
}
showLocalStorageUsageWarning(currentUsage) {
this.props.actions.addDangerToast(
t('SQL Lab uses your browser\'s local storage to store queries and results.' +
`\n Currently, you are using ${currentUsage.toFixed(2)} KB out of ${LOCALSTORAGE_MAX_USAGE_KB} KB. storage space.` +
'\n To keep SQL Lab from crashing, please delete some query tabs.' +
'\n You can re-access these queries by using the Save feature before you delete the tab. ' +
'Note that you will need to close other SQL Lab windows before you do this.'),
);
}
handleResize() {
this.setState({ contentHeight: this.getHeight() });
}
@ -91,8 +120,16 @@ class App extends React.PureComponent {
App.propTypes = {
actions: PropTypes.object,
localStorageUsageInKilobytes: PropTypes.number.isRequired,
};
function mapStateToProps(state) {
const { localStorageUsageInKilobytes } = state;
return {
localStorageUsageInKilobytes,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch),
@ -101,6 +138,6 @@ function mapDispatchToProps(dispatch) {
export { App };
export default connect(
null,
mapStateToProps,
mapDispatchToProps,
)(App);

View File

@ -27,7 +27,7 @@ import { t } from '@superset-ui/translation';
import * as Actions from '../actions/sqlLab';
import QueryHistory from './QueryHistory';
import ResultSet from './ResultSet';
import { STATUS_OPTIONS, STATE_BSSTYLE_MAP } from '../constants';
import { STATUS_OPTIONS, STATE_BSSTYLE_MAP, LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../constants';
const TAB_HEIGHT = 44;
@ -87,7 +87,8 @@ export class SouthPane extends React.PureComponent {
latestQuery = props.editorQueries.find(q => q.id === this.props.latestQueryId);
}
let results;
if (latestQuery) {
if (latestQuery &&
(Date.now() - latestQuery.startDttm) <= LOCALSTORAGE_MAX_QUERY_AGE_MS) {
results = (
<ResultSet
showControls

View File

@ -48,3 +48,13 @@ export const TIME_OPTIONS = [
export const SQL_EDITOR_GUTTER_HEIGHT = 5;
export const SQL_EDITOR_GUTTER_MARGIN = 3;
export const SQL_TOOLBAR_HEIGHT = 51;
// kilobyte storage
export const KB_STORAGE = 1024;
export const BYTES_PER_CHAR = 2;
// browser's localStorage max usage constants
export const LOCALSTORAGE_MAX_QUERY_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
export const LOCALSTORAGE_MAX_USAGE_KB = 5 * 1024; // 5M
export const LOCALSTORAGE_WARNING_THRESHOLD = 0.9;
export const LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS = 8000; // danger type toast duration

View File

@ -52,6 +52,7 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
messageToasts: getToastsFromPyFlashMessages(
(restBootstrapData.common || {}).flash_messages || [],
),
localStorageUsageInKilobytes: 0,
common: {
flash_messages: restBootstrapData.common.flash_messages,
conf: restBootstrapData.common.conf,

View File

@ -19,11 +19,13 @@
import { combineReducers } from 'redux';
import sqlLab from './sqlLab';
import localStorageUsageInKilobytes from './localStorageUsage';
import messageToasts from '../../messageToasts/reducers/index';
import common from './common';
export default combineReducers({
sqlLab,
localStorageUsageInKilobytes,
messageToasts,
common,
});

View File

@ -0,0 +1,21 @@
/**
* 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.
*/
export default function localStorageUsageReducer(state = 0) {
return state;
}

View File

@ -0,0 +1,37 @@
/**
* 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 { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../constants';
export default function emptyQueryResults(queries) {
return Object.keys(queries)
.reduce((accu, key) => {
const { startDttm, results } = queries[key];
const query = {
...queries[key],
results: Date.now() - startDttm > LOCALSTORAGE_MAX_QUERY_AGE_MS ?
{} : results,
};
const updatedQueries = {
...accu,
[key]: query,
};
return updatedQueries;
}, {});
}