-
-
-
- {this.props.visualize && (
-
- )}
- {this.props.csv && (
-
- )}
+
+ {this.props.visualize && (
+
+ )}
+ {this.props.csv && (
+
+ )}
-
- {t('Clipboard')}
-
- }
- />
-
-
-
- {this.props.search && (
-
- )}
-
+
+ {t('Clipboard')}
+
+ }
+ />
+ {this.props.search && (
+
+ )}
);
}
@@ -216,18 +211,38 @@ export default class ResultSet extends React.PureComponent {
);
} else if (query.state === 'success' && query.ctas) {
+ // Async queries
+ let tmpSchema = query.tempSchema;
+ let tmpTable = query.tempTableName;
+ // Sync queries, query.results.query contains the source of truth for them.
+ if (query.results && query.results.query) {
+ tmpTable = query.results.query.tempTable;
+ tmpSchema = query.results.query.tempSchema;
+ }
return (
- {t('Table')} [{query.tempTable}] {t('was created')}{' '}
-
-
+ {t('Table')} [
+
+ {tmpSchema}.{tmpTable}
+
+ ] {t('was created')}
+
+
+
+
);
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor.jsx b/superset-frontend/src/SqlLab/components/SqlEditor.jsx
index 138093f9f6c0..11edbf7c4d81 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor.jsx
@@ -20,6 +20,7 @@ import React from 'react';
import { CSSTransition } from 'react-transition-group';
import PropTypes from 'prop-types';
import {
+ Checkbox,
FormGroup,
InputGroup,
Form,
@@ -93,6 +94,7 @@ class SqlEditor extends React.PureComponent {
northPercent: props.queryEditor.northPercent || INITIAL_NORTH_PERCENT,
southPercent: props.queryEditor.southPercent || INITIAL_SOUTH_PERCENT,
sql: props.queryEditor.sql,
+ autocompleteEnabled: true,
};
this.sqlEditorRef = React.createRef();
this.northPaneRef = React.createRef();
@@ -245,6 +247,9 @@ class SqlEditor extends React.PureComponent {
handleWindowResize() {
this.setState({ height: this.getSqlEditorHeight() });
}
+ handleToggleAutocompleteEnabled = () => {
+ this.setState({ autocompleteEnabled: !this.state.autocompleteEnabled });
+ };
elementStyle(dimension, elementSize, gutterSize) {
return {
[dimension]: `calc(${elementSize}% - ${gutterSize +
@@ -337,6 +342,7 @@ class SqlEditor extends React.PureComponent {
+
+
+ {t('Autocomplete')}
+
+
{
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx
index b47d7921f05f..b4170b28b1d3 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx
@@ -39,6 +39,7 @@ const propTypes = {
databases: PropTypes.object.isRequired,
queries: PropTypes.object.isRequired,
queryEditors: PropTypes.array,
+ requestedQuery: PropTypes.object,
tabHistory: PropTypes.array.isRequired,
tables: PropTypes.array.isRequired,
offline: PropTypes.bool,
@@ -48,6 +49,7 @@ const propTypes = {
const defaultProps = {
queryEditors: [],
offline: false,
+ requestedQuery: null,
saveQueryWarning: null,
scheduleQueryWarning: null,
};
@@ -99,7 +101,12 @@ class TabbedSqlEditors extends React.PureComponent {
});
}
- const query = URI(window.location).search(true);
+ // merge post form data with GET search params
+ const query = {
+ ...this.props.requestedQuery,
+ ...URI(window.location).search(true),
+ };
+
// Popping a new tab based on the querystring
if (query.id || query.sql || query.savedQueryId || query.datasourceKey) {
if (query.id) {
@@ -374,7 +381,7 @@ class TabbedSqlEditors extends React.PureComponent {
TabbedSqlEditors.propTypes = propTypes;
TabbedSqlEditors.defaultProps = defaultProps;
-function mapStateToProps({ sqlLab, common }) {
+function mapStateToProps({ sqlLab, common, requestedQuery }) {
return {
databases: sqlLab.databases,
queryEditors: sqlLab.queryEditors,
@@ -388,6 +395,7 @@ function mapStateToProps({ sqlLab, common }) {
maxRow: common.conf.SQL_MAX_ROW,
saveQueryWarning: common.conf.SQLLAB_SAVE_WARNING_MESSAGE,
scheduleQueryWarning: common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE,
+ requestedQuery,
};
}
function mapDispatchToProps(dispatch) {
diff --git a/superset-frontend/src/SqlLab/main.less b/superset-frontend/src/SqlLab/main.less
index 230dbcf2a11f..19cd5f87489c 100644
--- a/superset-frontend/src/SqlLab/main.less
+++ b/superset-frontend/src/SqlLab/main.less
@@ -359,7 +359,21 @@ div.tablePopover {
}
.ResultSetControls {
+ display: flex;
+ justify-content: space-between;
padding: 8px 0;
+ position: fixed;
+}
+
+.ResultSetButtons {
+ display: grid;
+ grid-auto-flow: column;
+ grid-gap: 4px;
+ padding-right: 8px;
+}
+
+.filterable-table-container {
+ margin-top: 48px;
}
.ace_editor {
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.js b/superset-frontend/src/SqlLab/reducers/getInitialState.js
index e04f23b93cc0..fd3c8ae747d0 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.js
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.js
@@ -19,8 +19,16 @@
import { t } from '@superset-ui/translation';
import getToastsFromPyFlashMessages from '../../messageToasts/utils/getToastsFromPyFlashMessages';
-export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
- /*
+export default function getInitialState({
+ defaultDbId,
+ common,
+ active_tab: activeTab,
+ tab_state_ids: tabStateIds = [],
+ databases,
+ queries: queries_,
+ requested_query: requestedQuery,
+}) {
+ /**
* Before YYYY-MM-DD, the state for SQL Lab was stored exclusively in the
* browser's localStorage. The feature flag `SQLLAB_BACKEND_PERSISTENCE`
* moves the state to the backend instead, migrating it from local storage.
@@ -39,7 +47,7 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
autorun: false,
templateParams: null,
dbId: defaultDbId,
- queryLimit: restBootstrapData.common.conf.DEFAULT_SQLLAB_LIMIT,
+ queryLimit: common.conf.DEFAULT_SQLLAB_LIMIT,
validationResult: {
id: null,
errors: [],
@@ -52,11 +60,11 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
},
};
- /* Load state from the backend. This will be empty if the feature flag
+ /**
+ * Load state from the backend. This will be empty if the feature flag
* `SQLLAB_BACKEND_PERSISTENCE` is off.
*/
- const activeTab = restBootstrapData.active_tab;
- restBootstrapData.tab_state_ids.forEach(({ id, label }) => {
+ tabStateIds.forEach(({ id, label }) => {
let queryEditor;
if (activeTab && activeTab.id === id) {
queryEditor = {
@@ -92,7 +100,6 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
});
const tabHistory = activeTab ? [activeTab.id.toString()] : [];
-
const tables = [];
if (activeTab) {
activeTab.table_schemas
@@ -126,9 +133,10 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
});
}
- const { databases, queries } = restBootstrapData;
+ const queries = { ...queries_ };
- /* If the `SQLLAB_BACKEND_PERSISTENCE` feature flag is off, or if the user
+ /**
+ * If the `SQLLAB_BACKEND_PERSISTENCE` feature flag is off, or if the user
* hasn't used SQL Lab after it has been turned on, the state will be stored
* in the browser's local storage.
*/
@@ -173,13 +181,14 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
tables,
queriesLastUpdate: Date.now(),
},
+ requestedQuery,
messageToasts: getToastsFromPyFlashMessages(
- (restBootstrapData.common || {}).flash_messages || [],
+ (common || {}).flash_messages || [],
),
localStorageUsageInKilobytes: 0,
common: {
- flash_messages: restBootstrapData.common.flash_messages,
- conf: restBootstrapData.common.conf,
+ flash_messages: common.flash_messages,
+ conf: common.conf,
},
};
}
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js
index d751917e9f4e..3dbd45fe079f 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js
@@ -36,7 +36,7 @@ export default function sqlLabReducer(state = {}, action) {
[actions.ADD_QUERY_EDITOR]() {
const tabHistory = state.tabHistory.slice();
tabHistory.push(action.queryEditor.id);
- const newState = Object.assign({}, state, { tabHistory });
+ const newState = { ...state, tabHistory };
return addToArr(newState, 'queryEditors', action.queryEditor);
},
[actions.QUERY_EDITOR_SAVED]() {
@@ -102,19 +102,19 @@ export default function sqlLabReducer(state = {}, action) {
table => table.queryEditorId !== action.queryEditor.id,
);
- newState = Object.assign({}, newState, { tabHistory, tables, queries });
+ newState = { ...newState, tabHistory, tables, queries };
return newState;
},
[actions.REMOVE_QUERY]() {
- const newQueries = Object.assign({}, state.queries);
+ const newQueries = { ...state.queries };
delete newQueries[action.query.id];
- return Object.assign({}, state, { queries: newQueries });
+ return { ...state, queries: newQueries };
},
[actions.RESET_STATE]() {
- return Object.assign({}, getInitialState());
+ return { ...getInitialState() };
},
[actions.MERGE_TABLE]() {
- const at = Object.assign({}, action.table);
+ const at = { ...action.table };
let existingTable;
state.tables.forEach(xt => {
if (
@@ -146,32 +146,31 @@ export default function sqlLabReducer(state = {}, action) {
return alterInArr(state, 'tables', action.table, { expanded: true });
},
[actions.REMOVE_DATA_PREVIEW]() {
- const queries = Object.assign({}, state.queries);
+ const queries = { ...state.queries };
delete queries[action.table.dataPreviewQueryId];
const newState = alterInArr(state, 'tables', action.table, {
dataPreviewQueryId: null,
});
- return Object.assign({}, newState, { queries });
+ return { ...newState, queries };
},
[actions.CHANGE_DATA_PREVIEW_ID]() {
- const queries = Object.assign({}, state.queries);
+ const queries = { ...state.queries };
delete queries[action.oldQueryId];
const newTables = [];
state.tables.forEach(xt => {
if (xt.dataPreviewQueryId === action.oldQueryId) {
- newTables.push(
- Object.assign({}, xt, { dataPreviewQueryId: action.newQuery.id }),
- );
+ newTables.push({ ...xt, dataPreviewQueryId: action.newQuery.id });
} else {
newTables.push(xt);
}
});
- return Object.assign({}, state, {
+ return {
+ ...state,
queries,
tables: newTables,
activeSouthPaneTab: action.newQuery.id,
- });
+ };
},
[actions.COLLAPSE_TABLE]() {
return alterInArr(state, 'tables', action.table, { expanded: false });
@@ -180,7 +179,7 @@ export default function sqlLabReducer(state = {}, action) {
return removeFromArr(state, 'tables', action.table);
},
[actions.START_QUERY_VALIDATION]() {
- let newState = Object.assign({}, state);
+ let newState = { ...state };
const sqlEditor = { id: action.query.sqlEditorId };
newState = alterInArr(newState, 'queryEditors', sqlEditor, {
validationResult: {
@@ -204,7 +203,7 @@ export default function sqlLabReducer(state = {}, action) {
return state;
}
// Otherwise, persist the results on the queryEditor state
- let newState = Object.assign({}, state);
+ let newState = { ...state };
const sqlEditor = { id: action.query.sqlEditorId };
newState = alterInArr(newState, 'queryEditors', sqlEditor, {
validationResult: {
@@ -228,7 +227,7 @@ export default function sqlLabReducer(state = {}, action) {
return state;
}
// Otherwise, persist the results on the queryEditor state
- let newState = Object.assign({}, state);
+ let newState = { ...state };
const sqlEditor = { id: action.query.sqlEditorId };
newState = alterInArr(newState, 'queryEditors', sqlEditor, {
validationResult: {
@@ -247,7 +246,7 @@ export default function sqlLabReducer(state = {}, action) {
return newState;
},
[actions.COST_ESTIMATE_STARTED]() {
- let newState = Object.assign({}, state);
+ let newState = { ...state };
const sqlEditor = { id: action.query.sqlEditorId };
newState = alterInArr(newState, 'queryEditors', sqlEditor, {
queryCostEstimate: {
@@ -259,7 +258,7 @@ export default function sqlLabReducer(state = {}, action) {
return newState;
},
[actions.COST_ESTIMATE_RETURNED]() {
- let newState = Object.assign({}, state);
+ let newState = { ...state };
const sqlEditor = { id: action.query.sqlEditorId };
newState = alterInArr(newState, 'queryEditors', sqlEditor, {
queryCostEstimate: {
@@ -271,7 +270,7 @@ export default function sqlLabReducer(state = {}, action) {
return newState;
},
[actions.COST_ESTIMATE_FAILED]() {
- let newState = Object.assign({}, state);
+ let newState = { ...state };
const sqlEditor = { id: action.query.sqlEditorId };
newState = alterInArr(newState, 'queryEditors', sqlEditor, {
queryCostEstimate: {
@@ -283,23 +282,18 @@ export default function sqlLabReducer(state = {}, action) {
return newState;
},
[actions.START_QUERY]() {
- let newState = Object.assign({}, state);
+ let newState = { ...state };
if (action.query.sqlEditorId) {
const qe = getFromArr(state.queryEditors, action.query.sqlEditorId);
if (qe.latestQueryId && state.queries[qe.latestQueryId]) {
- const newResults = Object.assign(
- {},
- state.queries[qe.latestQueryId].results,
- {
- data: [],
- query: null,
- },
- );
- const q = Object.assign({}, state.queries[qe.latestQueryId], {
- results: newResults,
- });
- const queries = Object.assign({}, state.queries, { [q.id]: q });
- newState = Object.assign({}, state, { queries });
+ const newResults = {
+ ...state.queries[qe.latestQueryId].results,
+ data: [],
+ query: null,
+ };
+ const q = { ...state.queries[qe.latestQueryId], results: newResults };
+ const queries = { ...state.queries, [q.id]: q };
+ newState = { ...state, queries };
}
} else {
newState.activeSouthPaneTab = action.query.id;
@@ -317,7 +311,7 @@ export default function sqlLabReducer(state = {}, action) {
});
},
[actions.CLEAR_QUERY_RESULTS]() {
- const newResults = Object.assign({}, action.query.results);
+ const newResults = { ...action.query.results };
newResults.data = [];
return alterInObject(state, 'queries', action.query, {
results: newResults,
@@ -365,7 +359,7 @@ export default function sqlLabReducer(state = {}, action) {
) {
const tabHistory = state.tabHistory.slice();
tabHistory.push(action.queryEditor.id);
- return Object.assign({}, state, { tabHistory });
+ return { ...state, tabHistory };
}
return state;
},
@@ -378,7 +372,7 @@ export default function sqlLabReducer(state = {}, action) {
return extendArr(state, 'tables', action.tables);
},
[actions.SET_ACTIVE_SOUTHPANE_TAB]() {
- return Object.assign({}, state, { activeSouthPaneTab: action.tabId });
+ return { ...state, activeSouthPaneTab: action.tabId };
},
[actions.MIGRATE_QUERY_EDITOR]() {
// remove migrated query editor from localStorage
@@ -421,7 +415,7 @@ export default function sqlLabReducer(state = {}, action) {
tabId => tabId !== action.oldId,
);
tabHistory.push(action.newId);
- return Object.assign({}, state, { tabHistory });
+ return { ...state, tabHistory };
},
[actions.MIGRATE_QUERY]() {
const query = {
@@ -429,8 +423,8 @@ export default function sqlLabReducer(state = {}, action) {
// point query to migrated query editor
sqlEditorId: action.queryEditorId,
};
- const queries = Object.assign({}, state.queries, { [query.id]: query });
- return Object.assign({}, state, { queries });
+ const queries = { ...state.queries, [query.id]: query };
+ return { ...state, queries };
},
[actions.QUERY_EDITOR_SETDB]() {
return alterInArr(state, 'queryEditors', action.queryEditor, {
@@ -493,10 +487,10 @@ export default function sqlLabReducer(state = {}, action) {
action.databases.forEach(db => {
databases[db.id] = db;
});
- return Object.assign({}, state, { databases });
+ return { ...state, databases };
},
[actions.REFRESH_QUERIES]() {
- let newQueries = Object.assign({}, state.queries);
+ let newQueries = { ...state.queries };
// Fetch the updates to the queries present in the store.
let change = false;
let queriesLastUpdate = state.queriesLastUpdate;
@@ -510,39 +504,31 @@ export default function sqlLabReducer(state = {}, action) {
if (changedQuery.changedOn > queriesLastUpdate) {
queriesLastUpdate = changedQuery.changedOn;
}
- newQueries[id] = Object.assign({}, state.queries[id], changedQuery);
+ newQueries[id] = { ...state.queries[id], ...changedQuery };
change = true;
}
}
if (!change) {
newQueries = state.queries;
}
- return Object.assign({}, state, {
- queries: newQueries,
- queriesLastUpdate,
- });
+ return { ...state, queries: newQueries, queriesLastUpdate };
},
[actions.SET_USER_OFFLINE]() {
- return Object.assign({}, state, { offline: action.offline });
+ return { ...state, offline: action.offline };
},
[actions.CREATE_DATASOURCE_STARTED]() {
- return Object.assign({}, state, {
- isDatasourceLoading: true,
- errorMessage: null,
- });
+ return { ...state, isDatasourceLoading: true, errorMessage: null };
},
[actions.CREATE_DATASOURCE_SUCCESS]() {
- return Object.assign({}, state, {
+ return {
+ ...state,
isDatasourceLoading: false,
errorMessage: null,
datasource: action.datasource,
- });
+ };
},
[actions.CREATE_DATASOURCE_FAILED]() {
- return Object.assign({}, state, {
- isDatasourceLoading: false,
- errorMessage: action.err,
- });
+ return { ...state, isDatasourceLoading: false, errorMessage: action.err };
},
};
if (action.type in actionHandlers) {
diff --git a/superset-frontend/src/addSlice/AddSliceContainer.jsx b/superset-frontend/src/addSlice/AddSliceContainer.jsx
index 106d33af9ad7..f304d6695b8c 100644
--- a/superset-frontend/src/addSlice/AddSliceContainer.jsx
+++ b/superset-frontend/src/addSlice/AddSliceContainer.jsx
@@ -80,53 +80,58 @@ export default class AddSliceContainer extends React.PureComponent {
render() {
return (
-
{t('Create a new chart')}}>
-
-
{t('Choose a datasource')}
-
-
-
-
-
{t('Choose a visualization type')}
-
-
-
-
-
-
-
+
+
+
+
+
+
);
diff --git a/superset-frontend/src/chart/Chart.jsx b/superset-frontend/src/chart/Chart.jsx
index 227b7af86b47..044f0d83fe82 100644
--- a/superset-frontend/src/chart/Chart.jsx
+++ b/superset-frontend/src/chart/Chart.jsx
@@ -74,6 +74,7 @@ const defaultProps = {
setControlValue() {},
triggerRender: false,
dashboardId: null,
+ chartStackTrace: null,
};
class Chart extends React.PureComponent {
diff --git a/superset-frontend/src/chart/ChartRenderer.jsx b/superset-frontend/src/chart/ChartRenderer.jsx
index 304644ede50f..a5cb332551d6 100644
--- a/superset-frontend/src/chart/ChartRenderer.jsx
+++ b/superset-frontend/src/chart/ChartRenderer.jsx
@@ -21,7 +21,6 @@ import { snakeCase } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import { SuperChart } from '@superset-ui/chart';
-import { Tooltip } from 'react-bootstrap';
import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger/LogUtils';
const propTypes = {
@@ -62,11 +61,8 @@ const defaultProps = {
class ChartRenderer extends React.Component {
constructor(props) {
super(props);
- this.state = {};
-
this.hasQueryResponseChange = false;
- this.setTooltip = this.setTooltip.bind(this);
this.handleAddFilter = this.handleAddFilter.bind(this);
this.handleRenderSuccess = this.handleRenderSuccess.bind(this);
this.handleRenderFailure = this.handleRenderFailure.bind(this);
@@ -76,13 +72,12 @@ class ChartRenderer extends React.Component {
onAddFilter: this.handleAddFilter,
onError: this.handleRenderFailure,
setControlValue: this.handleSetControlValue,
- setTooltip: this.setTooltip,
onFilterMenuOpen: this.props.onFilterMenuOpen,
onFilterMenuClose: this.props.onFilterMenuClose,
};
}
- shouldComponentUpdate(nextProps, nextState) {
+ shouldComponentUpdate(nextProps) {
const resultsReady =
nextProps.queryResponse &&
['success', 'rendered'].indexOf(nextProps.chartStatus) > -1 &&
@@ -98,9 +93,9 @@ class ChartRenderer extends React.Component {
nextProps.annotationData !== this.props.annotationData ||
nextProps.height !== this.props.height ||
nextProps.width !== this.props.width ||
- nextState.tooltip !== this.state.tooltip ||
nextProps.triggerRender ||
- nextProps.formData.color_scheme !== this.props.formData.color_scheme
+ nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
+ nextProps.cacheBusterProp !== this.props.cacheBusterProp
) {
return true;
}
@@ -108,10 +103,6 @@ class ChartRenderer extends React.Component {
return false;
}
- setTooltip(tooltip) {
- this.setState({ tooltip });
- }
-
handleAddFilter(col, vals, merge = true, refresh = true) {
this.props.addFilter(col, vals, merge, refresh);
}
@@ -164,33 +155,6 @@ class ChartRenderer extends React.Component {
}
}
- renderTooltip() {
- const { tooltip } = this.state;
- if (tooltip && tooltip.content) {
- return (
-
- {typeof tooltip.content === 'string' ? (
-
- ) : (
- tooltip.content
- )}
-
- );
- }
- return null;
- }
-
render() {
const {
chartAlert,
@@ -233,25 +197,25 @@ class ChartRenderer extends React.Component {
: snakeCaseVizType;
return (
- <>
- {this.renderTooltip()}
-
- >
+
);
}
}
diff --git a/superset-frontend/src/chart/chartAction.js b/superset-frontend/src/chart/chartAction.js
index c7a125f63163..0d6b03b1ea54 100644
--- a/superset-frontend/src/chart/chartAction.js
+++ b/superset-frontend/src/chart/chartAction.js
@@ -25,6 +25,7 @@ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import {
getExploreUrlAndPayload,
getAnnotationJsonUrl,
+ postForm,
} from '../explore/exploreUtils';
import {
requiresQuery,
@@ -358,14 +359,12 @@ export function redirectSQLLab(formData) {
postPayload: { form_data: formData },
})
.then(({ json }) => {
- const redirectUrl = new URL(window.location);
- redirectUrl.pathname = '/superset/sqllab';
- for (const key of redirectUrl.searchParams.keys()) {
- redirectUrl.searchParams.delete(key);
- }
- redirectUrl.searchParams.set('datasourceKey', formData.datasource);
- redirectUrl.searchParams.set('sql', json.query);
- window.open(redirectUrl.href, '_blank');
+ const redirectUrl = '/superset/sqllab';
+ const payload = {
+ datasourceKey: formData.datasource,
+ sql: json.query,
+ };
+ postForm(redirectUrl, payload);
})
.catch(() =>
dispatch(addDangerToast(t('An error occurred while loading the SQL'))),
diff --git a/superset-frontend/src/chart/chartReducer.js b/superset-frontend/src/chart/chartReducer.js
index 8ac7f1aefdc2..1409623dabae 100644
--- a/superset-frontend/src/chart/chartReducer.js
+++ b/superset-frontend/src/chart/chartReducer.js
@@ -49,6 +49,7 @@ export default function chartReducer(charts = {}, action) {
chartStatus: 'success',
queryResponse: action.queryResponse,
chartAlert: null,
+ chartUpdateEndTime: now(),
};
},
[actions.CHART_UPDATE_STARTED](state) {
diff --git a/superset-frontend/src/components/AlteredSliceTag.jsx b/superset-frontend/src/components/AlteredSliceTag.jsx
index 5f274042f157..dbba032e04b3 100644
--- a/superset-frontend/src/components/AlteredSliceTag.jsx
+++ b/superset-frontend/src/components/AlteredSliceTag.jsx
@@ -20,9 +20,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Table, Tr, Td, Thead, Th } from 'reactable-arc';
import { isEqual, isEmpty } from 'lodash';
+import { getChartControlPanelRegistry } from '@superset-ui/chart';
+import getControlsForVizType from 'src/utils/getControlsForVizType';
import { t } from '@superset-ui/translation';
import TooltipWrapper from './TooltipWrapper';
-import { controls } from '../explore/controls';
import ModalTrigger from './ModalTrigger';
import { safeStringify } from '../utils/safeStringify';
@@ -52,7 +53,10 @@ export default class AlteredSliceTag extends React.Component {
constructor(props) {
super(props);
const diffs = this.getDiffs(props);
- this.state = { diffs, hasDiffs: !isEmpty(diffs) };
+
+ const controlsMap = getControlsForVizType(this.props.origFormData.viz_type);
+
+ this.state = { diffs, hasDiffs: !isEmpty(diffs), controlsMap };
}
UNSAFE_componentWillReceiveProps(newProps) {
@@ -69,6 +73,7 @@ export default class AlteredSliceTag extends React.Component {
// current form data and the saved form data
const ofd = props.origFormData;
const cfd = props.currentFormData;
+
const fdKeys = Object.keys(cfd);
const diffs = {};
for (const fdKey of fdKeys) {
@@ -98,7 +103,10 @@ export default class AlteredSliceTag extends React.Component {
return 'N/A';
} else if (value === null) {
return 'null';
- } else if (controls[key] && controls[key].type === 'AdhocFilterControl') {
+ } else if (
+ this.state.controlsMap[key] &&
+ this.state.controlsMap[key].type === 'AdhocFilterControl'
+ ) {
if (!value.length) {
return '[]';
}
@@ -111,9 +119,15 @@ export default class AlteredSliceTag extends React.Component {
return `${v.subject} ${v.operator} ${filterVal}`;
})
.join(', ');
- } else if (controls[key] && controls[key].type === 'BoundsControl') {
+ } else if (
+ this.state.controlsMap[key] &&
+ this.state.controlsMap[key].type === 'BoundsControl'
+ ) {
return `Min: ${value[0]}, Max: ${value[1]}`;
- } else if (controls[key] && controls[key].type === 'CollectionControl') {
+ } else if (
+ this.state.controlsMap[key] &&
+ this.state.controlsMap[key].type === 'CollectionControl'
+ ) {
return value.map(v => safeStringify(v)).join(', ');
} else if (typeof value === 'boolean') {
return value ? 'true' : 'false';
@@ -133,7 +147,11 @@ export default class AlteredSliceTag extends React.Component {
|
{this.formatValue(diffs[key].before, key)} |
{this.formatValue(diffs[key].after, key)} |
diff --git a/superset-frontend/src/components/Button.jsx b/superset-frontend/src/components/Button.jsx
index 43fe49bb85ab..80be8498c6e2 100644
--- a/superset-frontend/src/components/Button.jsx
+++ b/superset-frontend/src/components/Button.jsx
@@ -42,7 +42,7 @@ const defaultProps = {
const BUTTON_WRAPPER_STYLE = { display: 'inline-block', cursor: 'not-allowed' };
export default function Button(props) {
- const buttonProps = Object.assign({}, props);
+ const buttonProps = { ...props };
const tooltip = props.tooltip;
const placement = props.placement;
delete buttonProps.tooltip;
diff --git a/superset-frontend/src/components/ListView/Filters.tsx b/superset-frontend/src/components/ListView/Filters.tsx
new file mode 100644
index 000000000000..25b2c5b0bd9d
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Filters.tsx
@@ -0,0 +1,191 @@
+/**
+ * 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 React, { useState } from 'react';
+import styled from '@emotion/styled';
+import { withTheme } from 'emotion-theming';
+
+import StyledSelect, { AsyncStyledSelect } from 'src/components/StyledSelect';
+import SearchInput from 'src/components/SearchInput';
+import { Filter, Filters, FilterValue, InternalFilter } from './types';
+
+interface BaseFilter {
+ Header: string;
+ initialValue: any;
+}
+interface SelectFilterProps extends BaseFilter {
+ onSelect: (selected: any) => any;
+ selects: Filter['selects'];
+ emptyLabel?: string;
+ fetchSelects?: Filter['fetchSelects'];
+}
+
+const FilterContainer = styled.div`
+ display: inline;
+ margin-right: 8px;
+`;
+
+const Title = styled.span`
+ font-weight: bold;
+`;
+
+const CLEAR_SELECT_FILTER_VALUE = 'CLEAR_SELECT_FILTER_VALUE';
+
+function SelectFilter({
+ Header,
+ selects = [],
+ emptyLabel = 'None',
+ initialValue,
+ onSelect,
+ fetchSelects,
+}: SelectFilterProps) {
+ const clearFilterSelect = {
+ label: emptyLabel,
+ value: CLEAR_SELECT_FILTER_VALUE,
+ };
+
+ const options = React.useMemo(() => [clearFilterSelect, ...selects], [
+ emptyLabel,
+ selects,
+ ]);
+
+ const [value, setValue] = useState(
+ typeof initialValue === 'undefined'
+ ? clearFilterSelect.value
+ : initialValue,
+ );
+ const onChange = (selected: { label: string; value: any } | null) => {
+ if (selected === null) return;
+ setValue(selected.value);
+ onSelect(
+ selected.value === CLEAR_SELECT_FILTER_VALUE ? undefined : selected.value,
+ );
+ };
+ const fetchAndFormatSelects = async () => {
+ if (!fetchSelects) return { options: [clearFilterSelect] };
+ const selectValues = await fetchSelects();
+ return { options: [clearFilterSelect, ...selectValues] };
+ };
+
+ return (
+
+ {Header}:
+ {fetchSelects ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+interface SearchHeaderProps extends BaseFilter {
+ Header: string;
+ onSubmit: (val: string) => void;
+}
+
+function SearchFilter({ Header, initialValue, onSubmit }: SearchHeaderProps) {
+ const [value, setValue] = useState(initialValue || '');
+ const handleSubmit = () => onSubmit(value);
+
+ return (
+
+ {
+ setValue(e.currentTarget.value);
+ }}
+ onKeyDown={e => {
+ if (e.key === 'Enter') {
+ handleSubmit();
+ }
+ }}
+ onBlur={handleSubmit}
+ />
+
+ );
+}
+
+interface UIFiltersProps {
+ filters: Filters;
+ internalFilters: InternalFilter[];
+ updateFilterValue: (id: number, value: FilterValue['value']) => void;
+}
+
+const FilterWrapper = styled.div`
+ padding: 24px 16px 8px;
+`;
+
+function UIFilters({
+ filters,
+ internalFilters = [],
+ updateFilterValue,
+}: UIFiltersProps) {
+ return (
+
+ {filters.map(
+ ({ Header, input, selects, unfilteredLabel, fetchSelects }, index) => {
+ const initialValue =
+ internalFilters[index] && internalFilters[index].value;
+ if (input === 'select') {
+ return (
+ updateFilterValue(index, value)}
+ />
+ );
+ }
+ if (input === 'search') {
+ return (
+ updateFilterValue(index, value)}
+ />
+ );
+ }
+ return null;
+ },
+ )}
+
+ );
+}
+
+export default withTheme(UIFilters);
diff --git a/superset-frontend/src/components/ListView/LegacyFilters.tsx b/superset-frontend/src/components/ListView/LegacyFilters.tsx
new file mode 100644
index 000000000000..6c493bc63669
--- /dev/null
+++ b/superset-frontend/src/components/ListView/LegacyFilters.tsx
@@ -0,0 +1,199 @@
+/**
+ * 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 { t } from '@superset-ui/translation';
+import React, { Dispatch, SetStateAction } from 'react';
+import {
+ Button,
+ Col,
+ DropdownButton,
+ FormControl,
+ MenuItem,
+ Row,
+ // @ts-ignore
+} from 'react-bootstrap';
+// @ts-ignore
+import SelectComponent from 'react-select';
+// @ts-ignore
+import VirtualizedSelect from 'react-virtualized-select';
+import { Filters, InternalFilter, Select } from './types';
+import { extractInputValue, getDefaultFilterOperator } from './utils';
+
+export const FilterMenu = ({
+ filters,
+ internalFilters,
+ setInternalFilters,
+}: {
+ filters: Filters;
+ internalFilters: InternalFilter[];
+ setInternalFilters: Dispatch>;
+}) => (
+
+
+
+ {' '}
+ {t('Filter List')}
+ >
+ }
+ >
+ {filters
+ .map(({ id, Header }) => ({
+ Header,
+ id,
+ value: undefined,
+ }))
+ .map(ft => (
+
+ ))}
+
+
+);
+
+export const FilterInputs = ({
+ internalFilters,
+ filters,
+ updateInternalFilter,
+ removeFilterAndApply,
+ filtersApplied,
+ applyFilters,
+}: {
+ internalFilters: InternalFilter[];
+ filters: Filters;
+ updateInternalFilter: (i: number, f: object) => void;
+ removeFilterAndApply: (i: number) => void;
+ filtersApplied: boolean;
+ applyFilters: () => void;
+}) => (
+ <>
+ {internalFilters.map((ft, i) => {
+ const filter = filters.find(f => f.id === ft.id);
+ if (!filter) {
+ console.error(`could not find filter for ${ft.id}`);
+ return null;
+ }
+ return (
+
+
+
+ {ft.Header}
+
+
+ ) => {
+ updateInternalFilter(i, {
+ operator: e.currentTarget.value,
+ });
+ }}
+ >
+ {(filter.operators || []).map(({ label, value }: Select) => (
+
+ ))}
+
+
+
+
+ {filter.input === 'select' && (
+ {
+ updateInternalFilter(i, {
+ operator: ft.operator || getDefaultFilterOperator(filter),
+ value: e ? e.map(s => s.value) : e,
+ });
+ }}
+ />
+ )}
+ {filter.input !== 'select' && (
+ ) => {
+ e.persist();
+ updateInternalFilter(i, {
+ operator: ft.operator || getDefaultFilterOperator(filter),
+ value: extractInputValue(filter.input, e),
+ });
+ }}
+ />
+ )}
+
+
+ removeFilterAndApply(i)}
+ >
+
+
+
+
+
+
+ );
+ })}
+ {internalFilters.length > 0 && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+ >
+);
diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx
index aff559e0b20a..2999d7818d96 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -19,12 +19,9 @@
import { t } from '@superset-ui/translation';
import React, { FunctionComponent } from 'react';
import {
- Button,
Col,
DropdownButton,
- FormControl,
MenuItem,
- Pagination,
Row,
// @ts-ignore
} from 'react-bootstrap';
@@ -33,22 +30,14 @@ import SelectComponent from 'react-select';
// @ts-ignore
import VirtualizedSelect from 'react-virtualized-select';
import IndeterminateCheckbox from '../IndeterminateCheckbox';
-import './ListViewStyles.less';
import TableCollection from './TableCollection';
-import {
- FetchDataConfig,
- Filters,
- InternalFilter,
- Select,
- SortColumn,
-} from './types';
-import {
- convertFilters,
- extractInputValue,
- ListViewError,
- removeFromList,
- useListViewState,
-} from './utils';
+import Pagination from './Pagination';
+import { FilterMenu, FilterInputs } from './LegacyFilters';
+import FilterControls from './Filters';
+import { FetchDataConfig, Filters, SortColumn } from './types';
+import { ListViewError, useListViewState } from './utils';
+
+import './ListViewStyles.less';
interface Props {
columns: any[];
@@ -66,6 +55,7 @@ interface Props {
name: React.ReactNode;
onSelect: (rows: any[]) => any;
}>;
+ useNewUIFilters?: boolean;
}
const bulkSelectColumnConfig = {
@@ -94,6 +84,7 @@ const ListView: FunctionComponent = ({
title = '',
filters = [],
bulkActions = [],
+ useNewUIFilters = false,
}) => {
const {
getTableProps,
@@ -101,13 +92,12 @@ const ListView: FunctionComponent = ({
headerGroups,
rows,
prepareRow,
- canPreviousPage,
- canNextPage,
pageCount = 1,
gotoPage,
- setAllFilters,
+ removeFilterAndApply,
setInternalFilters,
updateInternalFilter,
+ applyFilterValue,
applyFilters,
filtersApplied,
selectedFlatRows,
@@ -121,6 +111,7 @@ const ListView: FunctionComponent = ({
fetchData,
initialPageSize,
initialSort,
+ initialFilters: useNewUIFilters ? filters : [],
});
const filterable = Boolean(filters.length);
if (filterable) {
@@ -137,161 +128,56 @@ const ListView: FunctionComponent = ({
});
}
- const removeFilterAndApply = (index: number) => {
- const updated = removeFromList(internalFilters, index);
- setInternalFilters(updated);
- setAllFilters(convertFilters(updated));
- };
-
return (
- {title && filterable && (
-
-
-
- {t(title)}
-
- {filterable && (
-
-
-
-
- {' '}
- {t('Filter List')}
- >
- }
- >
- {filters
- .map(({ id, Header }) => ({
- Header,
- id,
- }))
- .map((ft: InternalFilter) => (
-
- ))}
-
-
-
- )}
-
-
- {internalFilters.map((ft, i) => {
- const filter = filters.find(f => f.id === ft.id);
- if (!filter) {
- console.error(`could not find filter for ${ft.id}`);
- return null;
- }
- return (
-
+
+ {!useNewUIFilters && (
+ <>
+ {title && filterable && (
+ <>
-
- {ft.Header}
+
+ {t(title)}
-
- ) => {
- updateInternalFilter(i, {
- operator: e.currentTarget.value,
- });
- }}
- >
- {filter.operators.map(({ label, value }: Select) => (
-
- ))}
-
-
-
-
- {filter.input === 'select' && (
- {
- updateInternalFilter(i, {
- operator: ft.operator || filter.operators[0].value,
- value: e ? e.map(s => s.value) : e,
- });
- }}
+ {filterable && (
+
+
- )}
- {filter.input !== 'select' && (
- ) => {
- e.persist();
- updateInternalFilter(i, {
- operator: ft.operator || filter.operators[0].value,
- value: extractInputValue(filter.input, e),
- });
- }}
- />
- )}
-
-
- removeFilterAndApply(i)}
- >
-
-
-
+
+ )}
-
-
- );
- })}
- {internalFilters.length > 0 && (
- <>
-
-
-
-
-
-
-
- >
- )}
-
- )}
+
+
+ >
+ )}
+ >
+ )}
+ {useNewUIFilters && (
+ <>
+
+
+ {t(title)}
+
+
+
+
+ >
+ )}
+
= ({
1}
- next={canNextPage}
- last={pageIndex < pageCount - 2}
- items={pageCount}
- activePage={pageIndex + 1}
- ellipsis
- boundaryLinks
- maxButtons={5}
- onSelect={(p: number) => gotoPage(p - 1)}
+ totalPages={pageCount || 0}
+ currentPage={pageCount ? pageIndex + 1 : 0}
+ onChange={(p: number) => gotoPage(p - 1)}
+ hideFirstAndLastPageLinks
/>
diff --git a/superset-frontend/src/components/ListView/ListViewStyles.less b/superset-frontend/src/components/ListView/ListViewStyles.less
index 20b27730516f..2a510c6c6da4 100644
--- a/superset-frontend/src/components/ListView/ListViewStyles.less
+++ b/superset-frontend/src/components/ListView/ListViewStyles.less
@@ -60,6 +60,10 @@
.action-button {
margin: 0 8px;
}
+
+ .table-cell {
+ word-break: break-all;
+ }
}
@keyframes shimmer {
diff --git a/superset-frontend/src/components/ListView/Pagination.tsx b/superset-frontend/src/components/ListView/Pagination.tsx
new file mode 100644
index 000000000000..03b8663dba27
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Pagination.tsx
@@ -0,0 +1,53 @@
+/**
+ * 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 React from 'react';
+// @ts-ignore
+import { Pagination } from 'react-bootstrap';
+import {
+ createUltimatePagination,
+ ITEM_TYPES,
+} from 'react-ultimate-pagination';
+
+const ListViewPagination = createUltimatePagination({
+ WrapperComponent: Pagination,
+ itemTypeToComponent: {
+ [ITEM_TYPES.PAGE]: ({ value, isActive, onClick }) => (
+
+ {value}
+
+ ),
+ [ITEM_TYPES.ELLIPSIS]: ({ isActive, onClick }) => (
+
+ ),
+ [ITEM_TYPES.FIRST_PAGE_LINK]: ({ isActive, onClick }) => (
+
+ ),
+ [ITEM_TYPES.PREVIOUS_PAGE_LINK]: ({ isActive, onClick }) => (
+
+ ),
+ [ITEM_TYPES.NEXT_PAGE_LINK]: ({ isActive, onClick }) => (
+
+ ),
+ [ITEM_TYPES.LAST_PAGE_LINK]: ({ isActive, onClick }) => (
+
+ ),
+ },
+});
+
+export default ListViewPagination;
diff --git a/superset-frontend/src/components/ListView/TableCollection.tsx b/superset-frontend/src/components/ListView/TableCollection.tsx
index 126a0570be5b..863e655ce6df 100644
--- a/superset-frontend/src/components/ListView/TableCollection.tsx
+++ b/superset-frontend/src/components/ListView/TableCollection.tsx
@@ -85,7 +85,11 @@ export default function TableCollection({
const columnCellProps = cell.column.cellProps || {};
return (
- |
+ |
{cell.render('Cell')}
|
);
diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts
index 294192db0c3c..76acae3b7a3e 100644
--- a/superset-frontend/src/components/ListView/types.ts
+++ b/superset-frontend/src/components/ListView/types.ts
@@ -31,9 +31,13 @@ export interface Select {
export interface Filter {
Header: string;
id: string;
- operators: Select[];
- input?: 'text' | 'textarea' | 'select' | 'checkbox';
+ operators?: Select[];
+ operator?: string;
+ input?: 'text' | 'textarea' | 'select' | 'checkbox' | 'search';
+ unfilteredLabel?: string;
selects?: Select[];
+ onFilterOpen?: () => void;
+ fetchSelects?: () => Promise;
}
export type Filters = Filter[];
@@ -41,7 +45,13 @@ export type Filters = Filter[];
export interface FilterValue {
id: string;
operator?: string;
- value: string | boolean | number;
+ value:
+ | string
+ | boolean
+ | number
+ | null
+ | undefined
+ | { datasource_id: number; datasource_type: string };
}
export interface FetchDataConfig {
@@ -52,7 +62,7 @@ export interface FetchDataConfig {
}
export interface InternalFilter extends FilterValue {
- Header: string;
+ Header?: string;
}
export interface FilterOperatorMap {
diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts
index d94703a301cc..6bc643a9edad 100644
--- a/superset-frontend/src/components/ListView/utils.ts
+++ b/superset-frontend/src/components/ListView/utils.ts
@@ -33,7 +33,13 @@ import {
useQueryParams,
} from 'use-query-params';
-import { FetchDataConfig, InternalFilter, SortColumn } from './types';
+import {
+ FetchDataConfig,
+ Filter,
+ FilterValue,
+ InternalFilter,
+ SortColumn,
+} from './types';
export class ListViewError extends Error {
name = 'ListViewError';
@@ -55,17 +61,22 @@ function updateInList(list: any[], index: number, update: any): any[] {
];
}
+function mergeCreateFilterValues(list: Filter[], updateList: FilterValue[]) {
+ return list.map(({ id, operator }) => {
+ const update = updateList.find(obj => obj.id === id);
+
+ return { id, operator, value: update?.value };
+ });
+}
+
// convert filters from UI objects to data objects
-export function convertFilters(fts: InternalFilter[]) {
+export function convertFilters(fts: InternalFilter[]): FilterValue[] {
return fts
- .filter((ft: InternalFilter) => ft.value)
- .map(ft => ({ operator: ft.operator, ...ft }));
+ .filter(f => typeof f.value !== 'undefined')
+ .map(({ value, operator, id }) => ({ value, operator, id }));
}
-export function extractInputValue(
- inputType: 'text' | 'textarea' | 'checkbox' | 'select' | undefined,
- event: any,
-) {
+export function extractInputValue(inputType: Filter['input'], event: any) {
if (!inputType || inputType === 'text') {
return event.currentTarget.value;
}
@@ -76,6 +87,13 @@ export function extractInputValue(
return null;
}
+export function getDefaultFilterOperator(filter: Filter): string {
+ if (filter?.operator) return filter.operator;
+ if (filter?.operators?.length) {
+ return filter.operators[0].value;
+ }
+ return '';
+}
interface UseListViewConfig {
fetchData: (conf: FetchDataConfig) => any;
columns: any[];
@@ -84,6 +102,7 @@ interface UseListViewConfig {
initialPageSize: number;
initialSort?: SortColumn[];
bulkSelectMode?: boolean;
+ initialFilters?: Filter[];
bulkSelectColumnConfig?: {
id: string;
Header: (conf: any) => React.ReactNode;
@@ -97,6 +116,7 @@ export function useListViewState({
data,
count,
initialPageSize,
+ initialFilters = [],
initialSort = [],
bulkSelectMode = false,
bulkSelectColumnConfig,
@@ -123,10 +143,13 @@ export function useListViewState({
sortBy: initialSortBy,
};
- const columnsWithSelect = useMemo(
- () => (bulkSelectMode ? [bulkSelectColumnConfig, ...columns] : columns),
- [bulkSelectMode, columns],
- );
+ const columnsWithSelect = useMemo(() => {
+ // add exact filter type so filters with falsey values are not filtered out
+ const columnsWithFilter = columns.map(f => ({ ...f, filter: 'exact' }));
+ return bulkSelectMode
+ ? [bulkSelectColumnConfig, ...columnsWithFilter]
+ : columnsWithFilter;
+ }, [bulkSelectMode, columns]);
const {
getTableProps,
@@ -165,6 +188,14 @@ export function useListViewState({
query.filters || [],
);
+ useEffect(() => {
+ if (initialFilters.length) {
+ setInternalFilters(
+ mergeCreateFilterValues(initialFilters, query.filters),
+ );
+ }
+ }, [initialFilters]);
+
useEffect(() => {
const queryParams: any = {
filters: internalFilters,
@@ -175,22 +206,41 @@ export function useListViewState({
queryParams.sortOrder = sortBy[0].desc ? 'desc' : 'asc';
}
setQuery(queryParams);
-
fetchData({ pageIndex, pageSize, sortBy, filters });
}, [fetchData, pageIndex, pageSize, sortBy, filters]);
const filtersApplied = internalFilters.every(
({ id, value, operator }, index) =>
id &&
- filters[index] &&
- filters[index].id === id &&
- filters[index].value === value &&
+ filters[index]?.id === id &&
+ filters[index]?.value === value &&
// @ts-ignore
- filters[index].operator === operator,
+ filters[index]?.operator === operator,
);
+ const updateInternalFilter = (index: number, update: object) =>
+ setInternalFilters(updateInList(internalFilters, index, update));
+
+ const applyFilterValue = (index: number, value: any) => {
+ // skip redunundant updates
+ if (internalFilters[index].value === value) {
+ return;
+ }
+ const update = { ...internalFilters[index], value };
+ const updatedFilters = updateInList(internalFilters, index, update);
+ setInternalFilters(updatedFilters);
+ setAllFilters(convertFilters(updatedFilters));
+ };
+
+ const removeFilterAndApply = (index: number) => {
+ const updated = removeFromList(internalFilters, index);
+ setInternalFilters(updated);
+ setAllFilters(convertFilters(updated));
+ };
+
return {
applyFilters: () => setAllFilters(convertFilters(internalFilters)),
+ removeFilterAndApply,
canNextPage,
canPreviousPage,
filtersApplied,
@@ -205,7 +255,7 @@ export function useListViewState({
setAllFilters,
setInternalFilters,
state: { pageIndex, pageSize, sortBy, filters, internalFilters },
- updateInternalFilter: (index: number, update: object) =>
- setInternalFilters(updateInList(internalFilters, index, update)),
+ updateInternalFilter,
+ applyFilterValue,
};
}
diff --git a/superset-frontend/src/explore/validators.js b/superset-frontend/src/components/SearchInput.tsx
similarity index 51%
rename from superset-frontend/src/explore/validators.js
rename to superset-frontend/src/components/SearchInput.tsx
index 5cbdb21033c6..dc4e74451b43 100644
--- a/superset-frontend/src/explore/validators.js
+++ b/superset-frontend/src/components/SearchInput.tsx
@@ -16,36 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-/* Reusable validator functions used in controls definitions
- *
- * validator functions receive the v and the configuration of the control
- * as arguments and return something that evals to false if v is valid,
- * and an error message if not valid.
- * */
-import { t } from '@superset-ui/translation';
-
-export function numeric(v) {
- if (v && isNaN(v)) {
- return t('is expected to be a number');
- }
- return false;
-}
-
-export function integer(v) {
- if (v && (isNaN(v) || parseInt(v, 10) !== +v)) {
- return t('is expected to be an integer');
- }
- return false;
-}
+import styled from '@emotion/styled';
-export function nonEmpty(v) {
- if (
- v === null ||
- v === undefined ||
- v === '' ||
- (Array.isArray(v) && v.length === 0)
- ) {
- return t('cannot be empty');
- }
- return false;
-}
+export default styled.input`
+ background-color: #fff;
+ background-image: none;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ padding: 4px 8px;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+`;
diff --git a/superset-frontend/src/components/StyledSelect.tsx b/superset-frontend/src/components/StyledSelect.tsx
new file mode 100644
index 000000000000..79d9151fc66d
--- /dev/null
+++ b/superset-frontend/src/components/StyledSelect.tsx
@@ -0,0 +1,75 @@
+/**
+ * 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 styled from '@emotion/styled';
+// @ts-ignore
+import Select, { Async } from 'react-select';
+
+export default styled(Select)`
+ display: inline;
+ &.is-focused:not(.is-open) > .Select-control {
+ border: none;
+ box-shadow: none;
+ }
+ .Select-control {
+ display: inline-table;
+ border: none;
+ width: 100px;
+ &:focus,
+ &:hover {
+ border: none;
+ box-shadow: none;
+ }
+
+ .Select-arrow-zone {
+ padding-left: 10px;
+ }
+ }
+ .Select-menu-outer {
+ margin-top: 0;
+ border-bottom-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+`;
+
+export const AsyncStyledSelect = styled(Async)`
+ display: inline;
+ &.is-focused:not(.is-open) > .Select-control {
+ border: none;
+ box-shadow: none;
+ }
+ .Select-control {
+ display: inline-table;
+ border: none;
+ width: 100px;
+ &:focus,
+ &:hover {
+ border: none;
+ box-shadow: none;
+ }
+
+ .Select-arrow-zone {
+ padding-left: 10px;
+ }
+ }
+ .Select-menu-outer {
+ margin-top: 0;
+ border-bottom-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+`;
diff --git a/superset-frontend/src/components/VictoryTheme.js b/superset-frontend/src/components/VictoryTheme.js
index 54031a96e535..b673119eb6ec 100644
--- a/superset-frontend/src/components/VictoryTheme.js
+++ b/superset-frontend/src/components/VictoryTheme.js
@@ -65,116 +65,93 @@ const strokeLinejoin = 'round';
// Create the theme
const theme = {
- area: assign(
- {
- style: {
- data: {
- fill: charcoal,
- },
- labels: baseLabelStyles,
+ area: {
+ style: {
+ data: {
+ fill: charcoal,
},
+ labels: baseLabelStyles,
},
- baseProps,
- ),
- axis: assign(
- {
- style: {
- axis: {
- fill: 'none',
- stroke: AXIS_LINE_GRAY,
- strokeWidth: 1,
- strokeLinecap,
- strokeLinejoin,
- },
- axisLabel: assign({}, baseLabelStyles, {
- padding: 25,
- }),
- grid: {
- fill: 'none',
- stroke: 'transparent',
- },
- ticks: {
- fill: 'none',
- padding: 10,
- size: 1,
- stroke: 'transparent',
- },
- tickLabels: baseLabelStyles,
+ ...baseProps,
+ },
+ axis: {
+ style: {
+ axis: {
+ fill: 'none',
+ stroke: AXIS_LINE_GRAY,
+ strokeWidth: 1,
+ strokeLinecap,
+ strokeLinejoin,
},
- },
- baseProps,
- ),
- bar: assign(
- {
- style: {
- data: {
- fill: A11Y_BABU,
- padding: 10,
- stroke: 'transparent',
- strokeWidth: 0,
- width: 8,
- },
- labels: baseLabelStyles,
+ axisLabel: { ...baseLabelStyles, padding: 25 },
+ grid: {
+ fill: 'none',
+ stroke: 'transparent',
+ },
+ ticks: {
+ fill: 'none',
+ padding: 10,
+ size: 1,
+ stroke: 'transparent',
},
+ tickLabels: baseLabelStyles,
},
- baseProps,
- ),
- candlestick: assign(
- {
- style: {
- data: {
- stroke: A11Y_BABU,
- strokeWidth: 1,
- },
- labels: assign({}, baseLabelStyles, {
- padding: 25,
- textAnchor: 'end',
- }),
+ ...baseProps,
+ },
+ bar: {
+ style: {
+ data: {
+ fill: A11Y_BABU,
+ padding: 10,
+ stroke: 'transparent',
+ strokeWidth: 0,
+ width: 8,
},
- candleColors: {
- positive: '#ffffff',
- negative: charcoal,
+ labels: baseLabelStyles,
+ },
+ ...baseProps,
+ },
+ candlestick: {
+ style: {
+ data: {
+ stroke: A11Y_BABU,
+ strokeWidth: 1,
},
+ labels: { ...baseLabelStyles, padding: 25, textAnchor: 'end' },
},
- baseProps,
- ),
+ candleColors: {
+ positive: '#ffffff',
+ negative: charcoal,
+ },
+ ...baseProps,
+ },
chart: baseProps,
- errorbar: assign(
- {
- style: {
- data: {
- fill: 'none',
- stroke: charcoal,
- strokeWidth: 2,
- },
- labels: assign({}, baseLabelStyles, {
- textAnchor: 'start',
- }),
+ errorbar: {
+ style: {
+ data: {
+ fill: 'none',
+ stroke: charcoal,
+ strokeWidth: 2,
},
+ labels: { ...baseLabelStyles, textAnchor: 'start' },
},
- baseProps,
- ),
- group: assign(
- {
- colorScale: colors,
- },
- baseProps,
- ),
- line: assign(
- {
- style: {
- data: {
- fill: 'none',
- stroke: A11Y_BABU,
- strokeWidth: 2,
- },
- labels: assign({}, baseLabelStyles, {
- textAnchor: 'start',
- }),
+ ...baseProps,
+ },
+ group: {
+ colorScale: colors,
+ ...baseProps,
+ },
+ line: {
+ style: {
+ data: {
+ fill: 'none',
+ stroke: A11Y_BABU,
+ strokeWidth: 2,
},
+ labels: { ...baseLabelStyles, textAnchor: 'start' },
},
- baseProps,
- ),
+ ...baseProps,
+ },
pie: {
style: {
data: {
@@ -182,37 +159,28 @@ const theme = {
stroke: 'none',
strokeWidth: 1,
},
- labels: assign({}, baseLabelStyles, {
- padding: 200,
- textAnchor: 'middle',
- }),
+ labels: { ...baseLabelStyles, padding: 200, textAnchor: 'middle' },
},
colorScale: colors,
width: 400,
height: 400,
padding: 50,
},
- scatter: assign(
- {
- style: {
- data: {
- fill: charcoal,
- stroke: 'transparent',
- strokeWidth: 0,
- },
- labels: assign({}, baseLabelStyles, {
- textAnchor: 'middle',
- }),
+ scatter: {
+ style: {
+ data: {
+ fill: charcoal,
+ stroke: 'transparent',
+ strokeWidth: 0,
},
+ labels: { ...baseLabelStyles, textAnchor: 'middle' },
},
- baseProps,
- ),
- stack: assign(
- {
- colorScale: colors,
- },
- baseProps,
- ),
+ ...baseProps,
+ },
+ stack: {
+ colorScale: colors,
+ ...baseProps,
+ },
};
export default theme;
diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx
index c30272c12f5e..baadcfcb9600 100644
--- a/superset-frontend/src/dashboard/App.jsx
+++ b/superset-frontend/src/dashboard/App.jsx
@@ -16,36 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { hot } from 'react-hot-loader/root';
import React from 'react';
-import thunk from 'redux-thunk';
-import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
-import { hot } from 'react-hot-loader/root';
-import { initFeatureFlags } from 'src/featureFlags';
-import { initEnhancer } from '../reduxUtils';
-import logger from '../middleware/loggerMiddleware';
import setupApp from '../setup/setupApp';
import setupPlugins from '../setup/setupPlugins';
import DashboardContainer from './containers/Dashboard';
-import getInitialState from './reducers/getInitialState';
-import rootReducer from './reducers/index';
setupApp();
setupPlugins();
-const appContainer = document.getElementById('app');
-const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
-initFeatureFlags(bootstrapData.common.feature_flags);
-const initState = getInitialState(bootstrapData);
-
-const store = createStore(
- rootReducer,
- initState,
- compose(applyMiddleware(thunk, logger), initEnhancer(false)),
-);
-
-const App = () => (
+const App = ({ store }) => (
diff --git a/superset-frontend/src/dashboard/components/FilterIndicatorsContainer.jsx b/superset-frontend/src/dashboard/components/FilterIndicatorsContainer.jsx
index 549d3838f044..632942e469ca 100644
--- a/superset-frontend/src/dashboard/components/FilterIndicatorsContainer.jsx
+++ b/superset-frontend/src/dashboard/components/FilterIndicatorsContainer.jsx
@@ -18,7 +18,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
-import { isEmpty } from 'lodash';
+import { isEmpty, isNil } from 'lodash';
import FilterIndicator from './FilterIndicator';
import FilterIndicatorGroup from './FilterIndicatorGroup';
@@ -101,6 +101,15 @@ export default class FilterIndicatorsContainer extends React.PureComponent {
chartId,
column: name,
});
+
+ // filter values could be single value or array of values
+ const values =
+ isNil(columns[name]) ||
+ (isDateFilter && columns[name] === 'No filter') ||
+ (Array.isArray(columns[name]) && columns[name].length === 0)
+ ? []
+ : [].concat(columns[name]);
+
const indicator = {
chartId,
colorCode: dashboardFiltersColorMap[colorMapKey],
@@ -110,11 +119,7 @@ export default class FilterIndicatorsContainer extends React.PureComponent {
isInstantFilter,
name,
label: labels[name] || name,
- values:
- isEmpty(columns[name]) ||
- (isDateFilter && columns[name] === 'No filter')
- ? []
- : [].concat(columns[name]),
+ values,
isFilterFieldActive:
chartId === filterFieldOnFocus.chartId &&
name === filterFieldOnFocus.column,
diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header.jsx
index 77aba1d91cab..8ed90a6af4e5 100644
--- a/superset-frontend/src/dashboard/components/Header.jsx
+++ b/superset-frontend/src/dashboard/components/Header.jsx
@@ -449,7 +449,7 @@ class Header extends React.PureComponent {
{this.state.showingPropertiesModal && (
{
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal.jsx b/superset-frontend/src/dashboard/components/PropertiesModal.jsx
index f266465b8c57..9269299a9c21 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal.jsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal.jsx
@@ -20,8 +20,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Row, Col, Button, Modal, FormControl } from 'react-bootstrap';
import Dialog from 'react-bootstrap-dialog';
-import Select from 'react-select';
+import { Async as SelectAsync } from 'react-select';
import AceEditor from 'react-ace';
+import rison from 'rison';
import { t } from '@superset-ui/translation';
import { SupersetClient } from '@superset-ui/connection';
import '../stylesheets/buttons.less';
@@ -55,7 +56,6 @@ class PropertiesModal extends React.PureComponent {
json_metadata: '',
},
isDashboardLoaded: false,
- ownerOptions: null,
isAdvancedOpen: false,
};
this.onChange = this.onChange.bind(this);
@@ -63,10 +63,11 @@ class PropertiesModal extends React.PureComponent {
this.onOwnersChange = this.onOwnersChange.bind(this);
this.save = this.save.bind(this);
this.toggleAdvanced = this.toggleAdvanced.bind(this);
+ this.loadOwnerOptions = this.loadOwnerOptions.bind(this);
+ this.handleErrorResponse = this.handleErrorResponse.bind(this);
}
componentDidMount() {
- this.fetchOwnerOptions();
this.fetchDashboardDetails();
}
@@ -90,41 +91,42 @@ class PropertiesModal extends React.PureComponent {
// datamodel, the dashboard could probably just be passed as a prop.
SupersetClient.get({
endpoint: `/api/v1/dashboard/${this.props.dashboardId}`,
- })
- .then(response => {
- const dashboard = response.json.result;
- this.setState(state => ({
- isDashboardLoaded: true,
- values: {
- ...state.values,
- dashboard_title: dashboard.dashboard_title || '',
- slug: dashboard.slug || '',
- json_metadata: dashboard.json_metadata || '',
- },
- }));
- const initialSelectedValues = dashboard.owners.map(owner => ({
- value: owner.id,
- label: owner.username,
- }));
- this.onOwnersChange(initialSelectedValues);
- })
- .catch(err => console.error(err));
+ }).then(response => {
+ const dashboard = response.json.result;
+ this.setState(state => ({
+ isDashboardLoaded: true,
+ values: {
+ ...state.values,
+ dashboard_title: dashboard.dashboard_title || '',
+ slug: dashboard.slug || '',
+ json_metadata: dashboard.json_metadata || '',
+ },
+ }));
+ const initialSelectedOwners = dashboard.owners.map(owner => ({
+ value: owner.id,
+ label: `${owner.first_name} ${owner.last_name}`,
+ }));
+ this.onOwnersChange(initialSelectedOwners);
+ }, this.handleErrorResponse);
}
- fetchOwnerOptions() {
- SupersetClient.get({
- endpoint: `/api/v1/dashboard/related/owners`,
- })
- .then(response => {
+ loadOwnerOptions(input = '') {
+ const query = rison.encode({ filter: input });
+ return SupersetClient.get({
+ endpoint: `/api/v1/dashboard/related/owners?q=${query}`,
+ }).then(
+ response => {
const options = response.json.result.map(item => ({
value: item.value,
label: item.text,
}));
- this.setState({
- ownerOptions: options,
- });
- })
- .catch(err => console.error(err));
+ return { options };
+ },
+ badResponse => {
+ this.handleErrorResponse(badResponse);
+ return { options: [] };
+ },
+ );
}
updateFormState(name, value) {
@@ -142,6 +144,17 @@ class PropertiesModal extends React.PureComponent {
}));
}
+ async handleErrorResponse(response) {
+ const { error, statusText } = await getClientErrorObject(response);
+ this.dialog.show({
+ title: 'Error',
+ bsSize: 'medium',
+ bsStyle: 'danger',
+ actions: [Dialog.DefaultAction('Ok', () => {}, 'btn-danger')],
+ body: error || statusText || t('An error has occurred'),
+ });
+ }
+
save(e) {
e.preventDefault();
e.stopPropagation();
@@ -157,38 +170,21 @@ class PropertiesModal extends React.PureComponent {
json_metadata: values.json_metadata || null,
owners,
}),
- })
- .then(({ json }) => {
- this.props.addSuccessToast(t('The dashboard has been saved'));
- this.props.onDashboardSave({
- id: this.props.dashboardId,
- title: json.result.dashboard_title,
- slug: json.result.slug,
- jsonMetadata: json.result.json_metadata,
- ownerIds: json.result.owners,
- });
- this.props.onHide();
- })
- .catch(response =>
- getClientErrorObject(response).then(({ error, statusText }) => {
- this.dialog.show({
- title: 'Error',
- bsSize: 'medium',
- bsStyle: 'danger',
- actions: [Dialog.DefaultAction('Ok', () => {}, 'btn-danger')],
- body: error || statusText || t('An error has occurred'),
- });
- }),
- );
+ }).then(({ json }) => {
+ this.props.addSuccessToast(t('The dashboard has been saved'));
+ this.props.onDashboardSave({
+ id: this.props.dashboardId,
+ title: json.result.dashboard_title,
+ slug: json.result.slug,
+ jsonMetadata: json.result.json_metadata,
+ ownerIds: json.result.owners,
+ });
+ this.props.onHide();
+ }, this.handleErrorResponse);
}
render() {
- const {
- ownerOptions,
- values,
- isDashboardLoaded,
- isAdvancedOpen,
- } = this.state;
+ const { values, isDashboardLoaded, isAdvancedOpen } = this.state;
return (