diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 024c884619dd..0e0bb44d0688 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -40,6 +40,7 @@ import { import { UPDATE_COMPONENTS_PARENTS_LIST } from '../actions/dashboardLayout'; import serializeActiveFilterValues from '../util/serializeActiveFilterValues'; import serializeFilterScopes from '../util/serializeFilterScopes'; +import isFilterChart from '../util/isFilterChart'; import { getActiveFilters } from '../util/activeDashboardFilters'; import { safeStringify } from '../../utils/safeStringify'; @@ -285,7 +286,7 @@ export function addSliceToDashboard(id, component) { ]).then(() => { dispatch(addSlice(selectedSlice)); - if (selectedSlice && selectedSlice.viz_type === 'filter_box') { + if (selectedSlice && isFilterChart(selectedSlice)) { dispatch(addFilter(id, component, selectedSlice.form_data)); } }); @@ -295,7 +296,7 @@ export function addSliceToDashboard(id, component) { export function removeSliceFromDashboard(id) { return (dispatch, getState) => { const sliceEntity = getState().sliceEntities.slices[id]; - if (sliceEntity && sliceEntity.viz_type === 'filter_box') { + if (sliceEntity && isFilterChart(sliceEntity)) { dispatch(removeFilter(id)); } diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 770e24963dc9..3be42364a7e5 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -46,6 +46,7 @@ import getEmptyLayout from '../util/getEmptyLayout'; import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata'; import getLocationHash from '../util/getLocationHash'; import newComponentFactory from '../util/newComponentFactory'; +import isFilterChart from '../util/isFilterChart'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; export default function (bootstrapData) { @@ -107,7 +108,7 @@ export default function (bootstrapData) { const slices = {}; const sliceIds = new Set(); dashboard.slices.forEach(slice => { - const key = slice.slice_id; + const sliceId = slice.slice_id; if (['separator', 'markup'].indexOf(slice.form_data.viz_type) === -1) { const form_data = { ...slice.form_data, @@ -116,15 +117,15 @@ export default function (bootstrapData) { ...urlParams, }, }; - chartQueries[key] = { + chartQueries[sliceId] = { ...chart, - id: key, + id: sliceId, form_data, formData: applyDefaultFormData(form_data), }; - slices[key] = { - slice_id: key, + slices[sliceId] = { + slice_id: sliceId, slice_url: slice.slice_url, slice_name: slice.slice_name, form_data: slice.form_data, @@ -138,10 +139,10 @@ export default function (bootstrapData) { changed_on: new Date(slice.changed_on).getTime(), }; - sliceIds.add(key); + sliceIds.add(sliceId); // if there are newly added slices from explore view, fill slices into 1 or more rows - if (!chartIdToLayoutId[key] && layout[parentId]) { + if (!chartIdToLayoutId[sliceId] && layout[parentId]) { if ( newSlicesContainerWidth === 0 || newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT @@ -170,24 +171,15 @@ export default function (bootstrapData) { } // build DashboardFilters for interactive filter features - if (slice.form_data.viz_type === 'filter_box') { - const configs = getFilterConfigsFromFormdata(slice.form_data); - let columns = configs.columns; - const labels = configs.labels; - if (preselectFilters[key]) { - Object.keys(columns).forEach(col => { - if (preselectFilters[key][col]) { - columns = { - ...columns, - [col]: preselectFilters[key][col], - }; - } - }); - } + if (isFilterChart(slice)) { + const { columns, labels } = getFilterConfigsFromFormdata( + slice.form_data, + preselectFilters[sliceId], + ); const scopesByChartId = Object.keys(columns).reduce((map, column) => { const scopeSettings = { - ...filterScopes[key], + ...filterScopes[sliceId], }; const { scope, immune } = { ...DASHBOARD_FILTER_SCOPE_GLOBAL, @@ -203,12 +195,12 @@ export default function (bootstrapData) { }; }, {}); - const componentId = chartIdToLayoutId[key]; + const componentId = chartIdToLayoutId[sliceId]; const directPathToFilter = (layout[componentId].parents || []).slice(); directPathToFilter.push(componentId); - dashboardFilters[key] = { + dashboardFilters[sliceId] = { ...dashboardFilter, - chartId: key, + chartId: sliceId, componentId, datasourceId: slice.form_data.datasource, filterName: slice.slice_name, @@ -225,7 +217,7 @@ export default function (bootstrapData) { // sync layout names with current slice names in case a slice was edited // in explore since the layout was updated. name updates go through layout for undo/redo // functionality and python updates slice names based on layout upon dashboard save - const layoutId = chartIdToLayoutId[key]; + const layoutId = chartIdToLayoutId[sliceId]; if (layoutId && layout[layoutId]) { layout[layoutId].meta.sliceName = slice.slice_name; } diff --git a/superset-frontend/src/dashboard/util/getFilterConfigsFromFormdata.js b/superset-frontend/src/dashboard/util/getFilterConfigsFromFormdata.js index 192f7cbc0294..aadcda256ca8 100644 --- a/superset-frontend/src/dashboard/util/getFilterConfigsFromFormdata.js +++ b/superset-frontend/src/dashboard/util/getFilterConfigsFromFormdata.js @@ -18,12 +18,29 @@ */ /* eslint-disable camelcase */ import { TIME_FILTER_MAP } from '../../visualizations/FilterBox/FilterBox'; -import { - FILTER_CONFIG_ATTRIBUTES, - TIME_FILTER_LABELS, -} from '../../explore/constants'; +import { TIME_FILTER_LABELS } from '../../explore/constants'; -export default function getFilterConfigsFromFormdata(form_data = {}) { +/** + * Parse filters for Table chart. All non-metric columns are considered + * filterable values. + */ +function getFilterConfigsFromTableChart(form_data = {}) { + const { groupby = [], all_columns = [] } = form_data; + const configs = { columns: {}, labels: {} }; + // `groupby` is from GROUP BY mode (aggregations) + // `all_columns` is from NOT GROUP BY mode (raw records) + const columns = groupby.concat(all_columns); + columns.forEach(column => { + configs.columns[column] = undefined; + configs.labels[column] = column; + }); + return configs; +} + +/** + * Parse filter configs for FilterBox. + */ +function getFilterConfigsFromFilterBox(form_data = {}) { const { date_filter, filter_configs = [], @@ -32,83 +49,56 @@ export default function getFilterConfigsFromFormdata(form_data = {}) { show_sqla_time_column, show_sqla_time_granularity, } = form_data; - let configs = filter_configs.reduce( - ({ columns, labels }, config) => { - let defaultValues = config[FILTER_CONFIG_ATTRIBUTES.DEFAULT_VALUE]; - // defaultValue could be ; separated values, - // could be null or '' - if ( - config[FILTER_CONFIG_ATTRIBUTES.DEFAULT_VALUE] && - config[FILTER_CONFIG_ATTRIBUTES.MULTIPLE] - ) { - defaultValues = config.defaultValue.split(';'); - } - const updatedColumns = { - ...columns, - [config.column]: config.vals || defaultValues, - }; - const updatedLabels = { - ...labels, - [config.column]: config.label, - }; + const configs = { columns: {}, labels: {} }; - return { - columns: updatedColumns, - labels: updatedLabels, - }; - }, - { columns: {}, labels: {} }, - ); + filter_configs.forEach(({ column, label, defaultValue, multiple, vals }) => { + // treat empty string as undefined, too + const defaultValues = + multiple && defaultValue ? defaultValue.split(';') : defaultValue; + configs.columns[column] = vals || defaultValues; + configs.labels[column] = label; + }); if (date_filter) { - let updatedColumns = { - ...configs.columns, - [TIME_FILTER_MAP.time_range]: form_data.time_range, - }; - const updatedLabels = { - ...configs.labels, - ...Object.entries(TIME_FILTER_MAP).reduce( - (map, [key, value]) => ({ - ...map, - [value]: TIME_FILTER_LABELS[key], - }), - {}, - ), - }; - + configs.columns[TIME_FILTER_MAP.time_range] = form_data.time_range; + // a map from frontend enum key to backend column + Object.entries(TIME_FILTER_MAP).forEach(([key, column]) => { + configs.labels[column] = TIME_FILTER_LABELS[key]; + }); if (show_sqla_time_granularity) { - updatedColumns = { - ...updatedColumns, - [TIME_FILTER_MAP.time_grain_sqla]: form_data.time_grain_sqla, - }; + configs.columns[TIME_FILTER_MAP.time_grain_sqla] = + form_data.time_grain_sqla; } - if (show_sqla_time_column) { - updatedColumns = { - ...updatedColumns, - [TIME_FILTER_MAP.granularity_sqla]: form_data.granularity_sqla, - }; + configs.columns[TIME_FILTER_MAP.granularity_sqla] = + form_data.granularity_sqla; } - if (show_druid_time_granularity) { - updatedColumns = { - ...updatedColumns, - [TIME_FILTER_MAP.granularity]: form_data.granularity, - }; + configs.columns[TIME_FILTER_MAP.granularity] = form_data.granularity; } - if (show_druid_time_origin) { - updatedColumns = { - ...updatedColumns, - [TIME_FILTER_MAP.druid_time_origin]: form_data.druid_time_origin, - }; + configs.columns[TIME_FILTER_MAP.druid_time_origin] = + form_data.druid_time_origin; } + } + return configs; +} - configs = { - ...configs, - columns: updatedColumns, - labels: updatedLabels, - }; +export default function getFilterConfigsFromFormdata( + form_data = {}, + filters = undefined, +) { + const configs = form_data.table_filter + ? getFilterConfigsFromTableChart(form_data) + : getFilterConfigsFromFilterBox(form_data); + + // if current chart has preselected filters, update it + if (filters) { + Object.keys(filters).forEach(column => { + if (column in configs.columns && filters[column]) { + configs.columns[column] = filters[column]; + } + }); } return configs; } diff --git a/superset-frontend/src/dashboard/util/isFilterChart.js b/superset-frontend/src/dashboard/util/isFilterChart.js new file mode 100644 index 000000000000..6fb428afcc6e --- /dev/null +++ b/superset-frontend/src/dashboard/util/isFilterChart.js @@ -0,0 +1,31 @@ +/** + * 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. + */ +/* eslint-disable camelcase */ + +/** + * Check if a chart is one of the filter chart types, i.e., FilterBox + * and any charts with `table_filter = TRUE`. + * + * TODO: change `table_filter` to a more generic name. + */ +export default function isFilterChart(slice) { + const { form_data = {} } = slice; + const vizType = slice.viz_type || form_data.viz_type; + return vizType === 'filter_box' || !!form_data.table_filter; +} diff --git a/superset-frontend/stylesheets/superset.less b/superset-frontend/stylesheets/superset.less index 1560d67173c8..23f2572f7eca 100644 --- a/superset-frontend/stylesheets/superset.less +++ b/superset-frontend/stylesheets/superset.less @@ -546,10 +546,6 @@ tr.reactable-column-header th.reactable-header-sortable { text-align: right; } -td.filtered { - background-color: lighten(desaturate(@brand-primary, 50%), 50%); -} - .table-name { font-size: @font-size-l; }