From 8e7f0237ab375be4adb4aa766d9b9180431fd94d Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Fri, 4 Jun 2021 18:12:22 -0700 Subject: [PATCH 01/28] fix: apply template_params on external_metadata (#14996) * fix: apply template_params on external_metadata * Fix test --- superset/connectors/sqla/models.py | 4 +++- tests/datasource_tests.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 0842b0e57e2d9..2f7b6d1498d1c 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -638,7 +638,9 @@ def external_metadata(self) -> List[Dict[str, str]]: db_engine_spec = self.db_engine_spec if self.sql: engine = self.database.get_sqla_engine(schema=self.schema) - sql = self.get_template_processor().process_template(self.sql) + sql = self.get_template_processor().process_template( + self.sql, **self.template_params_dict + ) parsed_query = ParsedQuery(sql) if not db_engine_spec.is_readonly_query(parsed_query): raise SupersetSecurityException( diff --git a/tests/datasource_tests.py b/tests/datasource_tests.py index 438079fc5be85..4ae3957f77480 100644 --- a/tests/datasource_tests.py +++ b/tests/datasource_tests.py @@ -73,6 +73,25 @@ def test_external_metadata_for_virtual_table(self): session.delete(table) session.commit() + def test_external_metadata_for_virtual_table_template_params(self): + self.login(username="admin") + session = db.session + table = SqlaTable( + table_name="dummy_sql_table_with_template_params", + database=get_example_database(), + sql="select {{ foo }} as intcol", + template_params=json.dumps({"foo": "123"}), + ) + session.add(table) + session.commit() + + table = self.get_table_by_name("dummy_sql_table_with_template_params") + url = f"/datasource/external_metadata/table/{table.id}/" + resp = self.get_json_resp(url) + assert {o.get("name") for o in resp} == {"intcol"} + session.delete(table) + session.commit() + def test_external_metadata_for_malicious_virtual_table(self): self.login(username="admin") table = SqlaTable( From ff903486a851760e108b2e841e6a17348b3a9523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CA=88=E1=B5=83=E1=B5=A2?= Date: Fri, 4 Jun 2021 18:44:27 -0700 Subject: [PATCH 02/28] docs: fix custom oauth config (#14997) --- docs/src/pages/docs/installation/configuring.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/docs/installation/configuring.mdx b/docs/src/pages/docs/installation/configuring.mdx index c725bf5950311..53340a5c9cdad 100644 --- a/docs/src/pages/docs/installation/configuring.mdx +++ b/docs/src/pages/docs/installation/configuring.mdx @@ -134,7 +134,7 @@ OAUTH_PROVIDERS = [ 'access_token_headers':{ # Additional headers for calls to access_token_url 'Authorization': 'Basic Base64EncodedClientIdAndSecret' }, - 'base_url':'https://myAuthorizationServer/oauth2AuthorizationServer/', + 'api_base_url':'https://myAuthorizationServer/oauth2AuthorizationServer/', 'access_token_url':'https://myAuthorizationServer/oauth2AuthorizationServer/token', 'authorize_url':'https://myAuthorizationServer/oauth2AuthorizationServer/authorize' } From e2d60159742beaffac03a661f8232d6de2d38fcd Mon Sep 17 00:00:00 2001 From: Bonifacio de Oliveira Date: Fri, 4 Jun 2021 23:29:02 -0300 Subject: [PATCH 03/28] fix typos (#14950) --- docs/installation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 7dff638b041e4..edfedce43965d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -433,7 +433,7 @@ For other strategies, check the `superset/tasks/cache.py` file. Caching Thumbnails ------------------ -This is an optional feature that can be turned on by activating it's feature flag on config: +This is an optional feature that can be turned on by activating its feature flag on config: .. code-block:: python @@ -972,7 +972,7 @@ environment variable: :: Event Logging ------------- -Superset by default logs special action event on it's database. These log can be accessed on the UI navigating to +Superset by default logs special action event on its database. These logs can be accessed on the UI navigating to "Security" -> "Action Log". You can freely customize these logs by implementing your own event log class. Example of a simple JSON to Stdout class:: @@ -1358,7 +1358,7 @@ The available validators and names can be found in `sql_validators/`. **Scheduling queries** You can optionally allow your users to schedule queries directly in SQL Lab. -This is done by addding extra metadata to saved queries, which are then picked +This is done by adding extra metadata to saved queries, which are then picked up by an external scheduled (like [Apache Airflow](https://airflow.apache.org/)). To allow scheduled queries, add the following to your `config.py`: From 313809104432c755102b769e6495d6119cf99c61 Mon Sep 17 00:00:00 2001 From: Brian Childress Date: Sun, 6 Jun 2021 12:56:14 -0400 Subject: [PATCH 04/28] Update index.mdx (#14990) Correcting typo --- docs/src/pages/docs/installation/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/docs/installation/index.mdx b/docs/src/pages/docs/installation/index.mdx index 601753917ad2b..81bbf600a28ab 100644 --- a/docs/src/pages/docs/installation/index.mdx +++ b/docs/src/pages/docs/installation/index.mdx @@ -79,7 +79,7 @@ The following is for users who want to configure how Superset starts up in Docke You can configure the Docker Compose settings for dev and non-dev mode with `docker/.env` and `docker/.env-non-dev` respectively. These environment files set the environment for most containers in the Docker Compose setup, and some variables affect multiple containers and others only single ones. -One important variable is `SUPERSET_LOAD_EXAMPLES` which determines whether the `superset_init` container will load example data and visualizations into the database and Superset. Thiese examples are quite helpful for most people, but probably unnecessary for experienced users. The loading process can sometimes take a few minutes and a good amount of CPU, so you may want to disable it on a resource-constrained device. +One important variable is `SUPERSET_LOAD_EXAMPLES` which determines whether the `superset_init` container will load example data and visualizations into the database and Superset. These examples are quite helpful for most people, but probably unnecessary for experienced users. The loading process can sometimes take a few minutes and a good amount of CPU, so you may want to disable it on a resource-constrained device. **Note:** Users often want to connect to other databases from Superset. Currently, the easiest way to do this is to modify the `docker-compose-non-dev.yml` file and add your database as a service that the other services depend on (via `x-superset-depends-on`). Others have attempted to set `network_mode: host` on the Superset services, but these generally break the installation, because the configuration requires use of the Docker Compose DNS resolver for the service names. If you have a good solution for this, let us know! From a90e16850dc5e4f276e6dd55e101ced7ac45dcdf Mon Sep 17 00:00:00 2001 From: Ke Zhu Date: Sun, 6 Jun 2021 22:26:13 -0400 Subject: [PATCH 05/28] docs: required information for OAuth2 configuration (#15010) --- docs/src/pages/docs/installation/configuring.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/pages/docs/installation/configuring.mdx b/docs/src/pages/docs/installation/configuring.mdx index 53340a5c9cdad..d4fc778f58896 100644 --- a/docs/src/pages/docs/installation/configuring.mdx +++ b/docs/src/pages/docs/installation/configuring.mdx @@ -113,6 +113,8 @@ RequestHeader set X-Forwarded-Proto "https" Beyond FAB supported providers (Github, Twitter, LinkedIn, Google, Azure, etc), its easy to connect Superset with other OAuth2 Authorization Server implementations that support “code” authorization. +Make sure the pip package [`Authlib`](https://authlib.org/) is installed on the webserver. + First, configure authorization in Superset `superset_config.py`. ```python @@ -175,6 +177,10 @@ from custom_sso_security_manager import CustomSsoSecurityManager CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager ``` +Notice that the redirect URL will be `https:///oauth-authorized/` +When configuring an OAuth2 authorization provider if needed. For instance, the redirect URL will +be `https:///oauth-authorized/egaSSO` for the above configuration. + ### Feature Flags To support a diverse set of users, Superset has some features that are not enabled by default. For From 1fc08523af97f8a36a0b18d643a3fe60df815ca4 Mon Sep 17 00:00:00 2001 From: simcha90 <56388545+simcha90@users.noreply.github.com> Date: Mon, 7 Jun 2021 13:41:19 +0300 Subject: [PATCH 06/28] feat(native-filters): Support default to first value in select filter (#14869) * fix:fix get permission function * feat: add async filters support * revert: revert ff * feat: add async filters support * fix: merge with master * fix: remove tests * lint: fix lint * fix: fix CR notes * fix: fix with master * test: fix tests * refactor: update logic for default first value * fix: get requiredFirst * fix: support instant * docs: update text * docs: fix comments * docs: update texts --- .../DashboardBuilder/DashboardBuilder.tsx | 50 ++----- .../components/DashboardBuilder/state.ts | 93 +++++++++++++ .../FilterBar/FilterControls/FilterValue.tsx | 2 +- .../nativeFilters/FilterBar/index.tsx | 21 ++- .../nativeFilters/FilterBar/state.ts | 6 + .../FiltersConfigForm/FiltersConfigForm.tsx | 9 +- .../FiltersConfigForm/getControlItemsMap.tsx | 100 ++++++++------ .../nativeFilters/FiltersConfigModal/types.ts | 3 + .../nativeFilters/FiltersConfigModal/utils.ts | 3 + .../components/nativeFilters/types.ts | 1 + superset-frontend/src/dataMask/reducer.ts | 2 +- .../Select/SelectFilterPlugin.test.tsx | 16 +++ .../components/Select/SelectFilterPlugin.tsx | 127 ++++++++++-------- .../filters/components/Select/controlPanel.ts | 5 +- .../src/filters/components/Select/types.ts | 2 +- 15 files changed, 296 insertions(+), 144 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/DashboardBuilder/state.ts diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 807fe46b75dea..9873dfbd4ff9e 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -18,7 +18,7 @@ */ /* eslint-env browser */ import cx from 'classnames'; -import React, { FC, useEffect, useState } from 'react'; +import React, { FC } from 'react'; import { Sticky, StickyContainer } from 'react-sticky'; import { JsonObject, styled } from '@superset-ui/core'; import ErrorBoundary from 'src/components/ErrorBoundary'; @@ -30,7 +30,6 @@ import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; import ToastPresenter from 'src/messageToasts/containers/ToastPresenter'; import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex'; -import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { URL_PARAMS } from 'src/constants'; import { useDispatch, useSelector } from 'react-redux'; import { getUrlParam } from 'src/utils/urlUtils'; @@ -47,11 +46,11 @@ import { DashboardStandaloneMode, } from 'src/dashboard/util/constants'; import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar'; +import Loading from 'src/components/Loading'; import { StickyVerticalBar } from '../StickyVerticalBar'; import { shouldFocusTabs, getRootLevelTabsComponent } from './utils'; -import { useFilters } from '../nativeFilters/FilterBar/state'; -import { Filter } from '../nativeFilters/types'; import DashboardContainer from './DashboardContainer'; +import { useNativeFilters } from './state'; const TABS_HEIGHT = 47; const HEADER_HEIGHT = 67; @@ -99,12 +98,6 @@ const DashboardBuilder: FC = () => { const dashboardLayout = useSelector( state => state.dashboardLayout.present, ); - const showNativeFilters = useSelector( - state => state.dashboardInfo.metadata?.show_native_filters, - ); - const canEdit = useSelector( - ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, - ); const editMode = useSelector( state => state.dashboardState.editMode, ); @@ -112,22 +105,6 @@ const DashboardBuilder: FC = () => { state => state.dashboardState.directPathToChild, ); - const filters = useFilters(); - const filterValues = Object.values(filters); - - const nativeFiltersEnabled = - showNativeFilters && - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - (canEdit || (!canEdit && filterValues.length !== 0)); - - const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState( - getUrlParam(URL_PARAMS.showFilters) ?? true, - ); - - const toggleDashboardFiltersOpen = (visible?: boolean) => { - setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen); - }; - const handleChangeTab = ({ pathToTabIndex, }: { @@ -161,15 +138,12 @@ const DashboardBuilder: FC = () => { (hideDashboardHeader ? 0 : HEADER_HEIGHT) + (topLevelTabs ? TABS_HEIGHT : 0); - useEffect(() => { - if ( - filterValues.length === 0 && - dashboardFiltersOpen && - nativeFiltersEnabled - ) { - toggleDashboardFiltersOpen(false); - } - }, [filterValues.length]); + const { + showDashboard, + dashboardFiltersOpen, + toggleDashboardFiltersOpen, + nativeFiltersEnabled, + } = useNativeFilters(); return ( = () => { )} - + {showDashboard ? ( + + ) : ( + + )} {editMode && } diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts new file mode 100644 index 0000000000000..874525d93d484 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts @@ -0,0 +1,93 @@ +/** + * 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 { useSelector } from 'react-redux'; +import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; +import { useEffect, useState } from 'react'; +import { URL_PARAMS } from 'src/constants'; +import { getUrlParam } from 'src/utils/urlUtils'; +import { RootState } from 'src/dashboard/types'; +import { + useFilters, + useNativeFiltersDataMask, +} from '../nativeFilters/FilterBar/state'; +import { Filter } from '../nativeFilters/types'; + +// eslint-disable-next-line import/prefer-default-export +export const useNativeFilters = () => { + const [isInitialized, setIsInitialized] = useState(false); + const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState( + getUrlParam(URL_PARAMS.showFilters) ?? true, + ); + const showNativeFilters = useSelector( + state => state.dashboardInfo.metadata?.show_native_filters, + ); + const canEdit = useSelector( + ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, + ); + + const filters = useFilters(); + const filterValues = Object.values(filters); + + const nativeFiltersEnabled = + showNativeFilters && + isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && + (canEdit || (!canEdit && filterValues.length !== 0)); + + const requiredFirstFilter = filterValues.filter( + ({ requiredFirst }) => requiredFirst, + ); + const dataMask = useNativeFiltersDataMask(); + const showDashboard = + isInitialized || + !nativeFiltersEnabled || + !( + nativeFiltersEnabled && + requiredFirstFilter.length && + requiredFirstFilter.find( + ({ id }) => dataMask[id]?.filterState?.value === undefined, + ) + ); + + const toggleDashboardFiltersOpen = (visible?: boolean) => { + setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen); + }; + + useEffect(() => { + if ( + filterValues.length === 0 && + dashboardFiltersOpen && + nativeFiltersEnabled + ) { + toggleDashboardFiltersOpen(false); + } + }, [filterValues.length]); + + useEffect(() => { + if (showDashboard) { + setIsInitialized(true); + } + }, [showDashboard]); + + return { + showDashboard, + dashboardFiltersOpen, + toggleDashboardFiltersOpen, + nativeFiltersEnabled, + }; +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx index 495f50f437bf4..bb071315c6289 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx @@ -74,7 +74,7 @@ const FilterValue: React.FC = ({ const { name: groupby } = column; const hasDataSource = !!datasetId; const [isLoading, setIsLoading] = useState(hasDataSource); - const [isRefreshing, setIsRefreshing] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(true); const dispatch = useDispatch(); useEffect(() => { const newFormData = getFormData({ diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx index 44a4f82aed723..631179b898292 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx @@ -18,7 +18,7 @@ */ /* eslint-disable no-param-reassign */ -import { HandlerFunction, styled, t } from '@superset-ui/core'; +import { DataMask, HandlerFunction, styled, t } from '@superset-ui/core'; import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import cx from 'classnames'; @@ -26,11 +26,7 @@ import Icon from 'src/components/Icon'; import { Tabs } from 'src/common/components'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { updateDataMask } from 'src/dataMask/actions'; -import { - DataMaskState, - DataMaskStateWithId, - DataMaskWithId, -} from 'src/dataMask/types'; +import { DataMaskStateWithId, DataMaskWithId } from 'src/dataMask/types'; import { useImmer } from 'use-immer'; import { areObjectsEqual } from 'src/reduxUtils'; import { testWithId } from 'src/utils/testUtils'; @@ -178,10 +174,21 @@ const FilterBar: React.FC = ({ const handleFilterSelectionChange = ( filter: Pick & Partial, - dataMask: Partial, + dataMask: Partial, ) => { setIsFilterSetChanged(tab !== TabIds.AllFilters); setDataMaskSelected(draft => { + // force instant updating on initialization for filters with `requiredFirst` is true or instant filters + if ( + (dataMaskSelected[filter.id] && filter.isInstant) || + // filterState.value === undefined - means that value not initialized + (dataMask.filterState?.value !== undefined && + dataMaskSelected[filter.id]?.filterState?.value === undefined && + filter.requiredFirst) + ) { + dispatch(updateDataMask(filter.id, dataMask)); + } + draft[filter.id] = { ...(getInitialDataMask(filter.id) as DataMaskWithId), ...dataMask, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts index 8edf7145000af..aa4894fd9ee69 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts @@ -76,6 +76,7 @@ export const useFilterUpdates = ( // Load filters after charts loaded export const useInitialization = () => { const [isInitialized, setIsInitialized] = useState(false); + const filters = useFilters(); const charts = useSelector(state => state.charts); // We need to know how much charts now shown on dashboard to know how many of all charts should be loaded @@ -90,6 +91,11 @@ export const useInitialization = () => { return; } + if (Object.values(filters).find(({ requiredFirst }) => requiredFirst)) { + setIsInitialized(true); + return; + } + // For some dashboards may be there are no charts on first page, // so we check up to 1 sec if there is at least on chart to load let filterTimeout: NodeJS.Timeout; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index 76fa33bdc7097..296ee8e6ad251 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -443,6 +443,7 @@ const FiltersConfigForm = ( filterId, filterType: formFilter.filterType, filterToEdit, + formFilter, }) : {}; @@ -592,6 +593,7 @@ const FiltersConfigForm = ( expandIconPosition="right" > @@ -625,7 +627,11 @@ const FiltersConfigForm = ( { validator: (rule, value) => { const hasValue = !!value.filterState?.value; - if (hasValue) { + if ( + hasValue || + // TODO: do more generic + formFilter.controlValues?.defaultToFirstItem + ) { return Promise.resolve(); } return Promise.reject( @@ -673,6 +679,7 @@ const FiltersConfigForm = ( {((hasDataset && hasAdditionalFilters) || hasMetrics) && ( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx index 15e877ee57478..097d80b8d84cb 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx @@ -23,10 +23,11 @@ import { import React from 'react'; import { Checkbox } from 'src/common/components'; import { FormInstance } from 'antd/lib/form'; -import { getChartControlPanelRegistry, t } from '@superset-ui/core'; +import { getChartControlPanelRegistry, styled, t } from '@superset-ui/core'; import { Tooltip } from 'src/components/Tooltip'; +import { FormItem } from 'src/components/Form'; import { getControlItems, setNativeFilterFieldValues } from './utils'; -import { NativeFiltersForm } from '../types'; +import { NativeFiltersForm, NativeFiltersFormItem } from '../types'; import { StyledRowFormItem } from './FiltersConfigForm'; import { Filter } from '../../types'; @@ -37,8 +38,13 @@ export interface ControlItemsProps { filterId: string; filterType: string; filterToEdit?: Filter; + formFilter?: NativeFiltersFormItem; } +const CleanFormItem = styled(FormItem)` + margin-bottom: 0; +`; + export default function getControlItemsMap({ disabled, forceUpdate, @@ -46,6 +52,7 @@ export default function getControlItemsMap({ filterId, filterType, filterToEdit, + formFilter, }: ControlItemsProps) { const controlPanelRegistry = getChartControlPanelRegistry(); const controlItems = @@ -66,46 +73,61 @@ export default function getControlItemsMap({ filterToEdit?.controlValues?.[controlItem.name] ?? controlItem?.config?.default; const element = ( - - + - + { + if (controlItem.config.requiredFirst) { + setNativeFilterFieldValues(form, filterId, { + requiredFirst: { + ...formFilter?.requiredFirst, + [controlItem.name]: checked, + }, + }); + } + if (controlItem.config.resetConfig) { + setNativeFilterFieldValues(form, filterId, { + defaultDataMask: null, + }); + } + forceUpdate(); + }} + > + {controlItem.config.label}{' '} + {controlItem.config.description && ( + + )} + + + + ); map[controlItem.name] = { element, checked: initialValue }; }); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts index 60051e73f0ab6..0ba091f4eebc1 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts @@ -31,6 +31,9 @@ export interface NativeFiltersFormItem { controlValues: { [key: string]: any; }; + requiredFirst: { + [key: string]: boolean; + }; defaultValue: any; defaultDataMask: DataMask; parentFilter: { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts index 852d4e15f661d..b218ad4944dc8 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts @@ -140,6 +140,9 @@ export const createHandleSave = ( adhoc_filters: formInputs.adhoc_filters, time_range: formInputs.time_range, controlValues: formInputs.controlValues ?? {}, + requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find( + rf => rf, + ), name: formInputs.name, filterType: formInputs.filterType, // for now there will only ever be one target diff --git a/superset-frontend/src/dashboard/components/nativeFilters/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/types.ts index ac772dcd73491..a1e206bab073d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/types.ts @@ -56,6 +56,7 @@ export interface Filter { sortMetric?: string | null; adhoc_filters?: AdhocFilter[]; time_range?: string; + requiredFirst?: boolean; tabsInScope?: string[]; chartsInScope?: number[]; } diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts index a0b0e38609db6..275787e0dba1d 100644 --- a/superset-frontend/src/dataMask/reducer.ts +++ b/superset-frontend/src/dataMask/reducer.ts @@ -49,7 +49,7 @@ export function getInitialDataMask(id: string): DataMaskWithId { ...otherProps, extraFormData: {}, filterState: { - value: null, + value: undefined, }, ownState: {}, } as DataMaskWithId; diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx index c42c1ce3e10bd..70cb4346fad27 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx @@ -88,6 +88,7 @@ describe('SelectFilterPlugin', () => { it('Add multiple values with first render', () => { getWrapper(); expect(setDataMask).toHaveBeenCalledWith({ + extraFormData: {}, filterState: { value: ['boy'], }, @@ -98,6 +99,9 @@ describe('SelectFilterPlugin', () => { }, }); expect(setDataMask).toHaveBeenCalledWith({ + __cache: { + value: ['boy'], + }, extraFormData: { filters: [ { @@ -120,6 +124,9 @@ describe('SelectFilterPlugin', () => { userEvent.click(screen.getByRole('combobox')); userEvent.click(screen.getByTitle('girl')); expect(setDataMask).toHaveBeenCalledWith({ + __cache: { + value: ['boy'], + }, extraFormData: { filters: [ { @@ -146,6 +153,9 @@ describe('SelectFilterPlugin', () => { getWrapper(); userEvent.click(document.querySelector('[data-icon="close"]')!); expect(setDataMask).toHaveBeenCalledWith({ + __cache: { + value: ['boy'], + }, extraFormData: { adhoc_filters: [ { @@ -171,6 +181,9 @@ describe('SelectFilterPlugin', () => { getWrapper({ enableEmptyFilter: false }); userEvent.click(document.querySelector('[data-icon="close"]')!); expect(setDataMask).toHaveBeenCalledWith({ + __cache: { + value: ['boy'], + }, extraFormData: {}, filterState: { label: '', @@ -189,6 +202,9 @@ describe('SelectFilterPlugin', () => { userEvent.click(screen.getByRole('combobox')); userEvent.click(screen.getByTitle('girl')); expect(setDataMask).toHaveBeenCalledWith({ + __cache: { + value: ['boy'], + }, extraFormData: { filters: [ { diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx index 2f7d5651c0166..4afdc1d2c86b6 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +/* eslint-disable no-param-reassign */ import { AppSection, DataMask, @@ -28,16 +29,11 @@ import { t, tn, } from '@superset-ui/core'; -import React, { - useCallback, - useEffect, - useMemo, - useReducer, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Select } from 'src/common/components'; import debounce from 'lodash/debounce'; import { SLOW_DEBOUNCE } from 'src/constants'; +import { useImmerReducer } from 'use-immer'; import Icons from 'src/components/Icons'; import { PluginFilterSelectProps, SelectValue } from './types'; import { StyledSelect, Styles } from '../common'; @@ -49,41 +45,33 @@ type DataMaskAction = | { type: 'ownState'; ownState: JsonObject } | { type: 'filterState'; + __cache: JsonObject; extraFormData: ExtraFormData; filterState: { value: SelectValue; label?: string }; }; -function reducer(state: DataMask, action: DataMaskAction): DataMask { +function reducer( + draft: Required & { __cache?: JsonObject }, + action: DataMaskAction, +) { switch (action.type) { case 'ownState': - return { - ...state, - ownState: { - ...(state.ownState || {}), - ...action.ownState, - }, + draft.ownState = { + ...draft.ownState, + ...action.ownState, }; + return draft; case 'filterState': - return { - ...state, - extraFormData: action.extraFormData, - filterState: { - ...(state.filterState || {}), - ...action.filterState, - }, - }; + draft.extraFormData = action.extraFormData; + // eslint-disable-next-line no-underscore-dangle + draft.__cache = action.__cache; + draft.filterState = { ...draft.filterState, ...action.filterState }; + return draft; default: - return { - ...state, - }; + return draft; } } -type DataMaskReducer = ( - prevState: DataMask, - action: DataMaskAction, -) => DataMask; - export default function PluginFilterSelect(props: PluginFilterSelectProps) { const { coltypeMap, @@ -127,32 +115,49 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { }, [col, selectedValues, data]); const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [currentSuggestionSearch, setCurrentSuggestionSearch] = useState(''); - const [dataMask, dispatchDataMask] = useReducer(reducer, { + const [dataMask, dispatchDataMask] = useImmerReducer(reducer, { + extraFormData: {}, filterState, ownState: { coltypeMap, }, }); - const updateDataMask = (values: SelectValue) => { - const emptyFilter = - enableEmptyFilter && !inverseSelection && !values?.length; - const suffix = - inverseSelection && values?.length ? ` (${t('excluded')})` : ''; + const updateDataMask = useCallback( + (values: SelectValue) => { + const emptyFilter = + enableEmptyFilter && !inverseSelection && !values?.length; - dispatchDataMask({ - type: 'filterState', - extraFormData: getSelectExtraFormData( - col, - values, - emptyFilter, - inverseSelection, - ), - filterState: { - value: values, - label: `${(values || []).join(', ')}${suffix}`, - }, - }); - }; + const suffix = + inverseSelection && values?.length ? ` (${t('excluded')})` : ''; + + dispatchDataMask({ + type: 'filterState', + __cache: filterState, + extraFormData: getSelectExtraFormData( + col, + values, + emptyFilter, + inverseSelection, + ), + filterState: { + label: `${(values || []).join(', ')}${suffix}`, + value: + appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem + ? undefined + : values, + }, + }); + }, + [ + appSection, + col, + defaultToFirstItem, + dispatchDataMask, + enableEmptyFilter, + inverseSelection, + JSON.stringify(filterState), + ], + ); useEffect(() => { if (!isDropdownVisible) { @@ -216,15 +221,19 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { }; useEffect(() => { - const firstItem: SelectValue = data[0] - ? (groupby.map(col => data[0][col]) as string[]) - : null; - if (isDisabled) { + if (defaultToFirstItem && filterState.value === undefined) { + // initialize to first value if set to default to first item + const firstItem: SelectValue = data[0] + ? (groupby.map(col => data[0][col]) as string[]) + : null; + // firstItem[0] !== undefined for a case when groupby changed but new data still not fetched + // TODO: still need repopulate default value in config modal when column changed + if (firstItem && firstItem[0] !== undefined) { + updateDataMask(firstItem); + } + } else if (isDisabled) { // empty selection if filter is disabled updateDataMask(null); - } else if (!isDisabled && defaultToFirstItem && firstItem) { - // initialize to first value if set to default to first item - updateDataMask(firstItem); } else { // reset data mask based on filter state updateDataMask(filterState.value); @@ -235,6 +244,10 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { defaultToFirstItem, enableEmptyFilter, inverseSelection, + updateDataMask, + data, + groupby, + JSON.stringify(filterState), ]); useEffect(() => { diff --git a/superset-frontend/src/filters/components/Select/controlPanel.ts b/superset-frontend/src/filters/components/Select/controlPanel.ts index 335e44483f888..74891b0ed9464 100644 --- a/superset-frontend/src/filters/components/Select/controlPanel.ts +++ b/superset-frontend/src/filters/components/Select/controlPanel.ts @@ -93,7 +93,10 @@ const config: ControlPanelConfig = { resetConfig: true, affectsDataMask: true, renderTrigger: true, - description: t('Select first item by default'), + requiredFirst: true, + description: t( + 'Select first item by default (when using this option, default value can’t be set)', + ), }, }, ], diff --git a/superset-frontend/src/filters/components/Select/types.ts b/superset-frontend/src/filters/components/Select/types.ts index aac5aa905a882..36052e8696a90 100644 --- a/superset-frontend/src/filters/components/Select/types.ts +++ b/superset-frontend/src/filters/components/Select/types.ts @@ -29,7 +29,7 @@ import { import { RefObject } from 'react'; import { PluginFilterHooks, PluginFilterStylesProps } from '../types'; -export type SelectValue = (number | string)[] | null; +export type SelectValue = (number | string)[] | null | undefined; interface PluginFilterSelectCustomizeProps { defaultValue?: SelectValue; From d2a6e8cd20274d7c8b850a973b3fc6869ee68792 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Mon, 7 Jun 2021 13:47:02 +0300 Subject: [PATCH 07/28] fix(native-filters): avoid double load on initialization (#15012) --- .../Select/SelectFilterPlugin.test.tsx | 51 +++++++++---------- .../components/Select/SelectFilterPlugin.tsx | 6 +-- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx index 70cb4346fad27..b29e64207be8b 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx @@ -30,7 +30,7 @@ const selectMultipleProps = { enableEmptyFilter: true, defaultToFirstItem: false, inverseSelection: false, - searchAllOptions: true, + searchAllOptions: false, datasource: '3__table', groupby: ['gender'], adhocFilters: [], @@ -48,7 +48,7 @@ const selectMultipleProps = { }, height: 20, hooks: {}, - ownState: { coltypeMap: { gender: 1 }, search: null }, + ownState: {}, filterState: { value: ['boy'] }, queriesData: [ { @@ -92,11 +92,6 @@ describe('SelectFilterPlugin', () => { filterState: { value: ['boy'], }, - ownState: { - coltypeMap: { - gender: 1, - }, - }, }); expect(setDataMask).toHaveBeenCalledWith({ __cache: { @@ -115,11 +110,6 @@ describe('SelectFilterPlugin', () => { label: 'boy', value: ['boy'], }, - ownState: { - coltypeMap: { - gender: 1, - }, - }, }); userEvent.click(screen.getByRole('combobox')); userEvent.click(screen.getByTitle('girl')); @@ -140,12 +130,6 @@ describe('SelectFilterPlugin', () => { label: 'boy, girl', value: ['boy', 'girl'], }, - ownState: { - coltypeMap: { - gender: 1, - }, - search: null, - }, }); }); @@ -169,11 +153,6 @@ describe('SelectFilterPlugin', () => { label: '', value: null, }, - ownState: { - coltypeMap: { - gender: 1, - }, - }, }); }); @@ -189,11 +168,6 @@ describe('SelectFilterPlugin', () => { label: '', value: null, }, - ownState: { - coltypeMap: { - gender: 1, - }, - }, }); }); @@ -218,6 +192,27 @@ describe('SelectFilterPlugin', () => { label: 'girl (excluded)', value: ['girl'], }, + }); + }); + + it('Add ownState with column types when search all options', () => { + getWrapper({ searchAllOptions: true, multiSelect: false }); + userEvent.click(screen.getByRole('combobox')); + userEvent.click(screen.getByTitle('girl')); + expect(setDataMask).toHaveBeenCalledWith({ + extraFormData: { + filters: [ + { + col: 'gender', + op: 'IN', + val: ['girl'], + }, + ], + }, + filterState: { + label: 'girl', + value: ['girl'], + }, ownState: { coltypeMap: { gender: 1, diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx index 4afdc1d2c86b6..d10544a485035 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx @@ -97,6 +97,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { } = formData; const groupby = ensureIsArray(formData.groupby); const [col] = groupby; + const [initialColtypeMap] = useState(coltypeMap); const [selectedValues, setSelectedValues] = useState( filterState.value, ); @@ -118,9 +119,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { const [dataMask, dispatchDataMask] = useImmerReducer(reducer, { extraFormData: {}, filterState, - ownState: { - coltypeMap, - }, }); const updateDataMask = useCallback( (values: SelectValue) => { @@ -174,6 +172,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { dispatchDataMask({ type: 'ownState', ownState: { + coltypeMap: initialColtypeMap, search: val, }, }); @@ -194,6 +193,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { dispatchDataMask({ type: 'ownState', ownState: { + coltypeMap: initialColtypeMap, search: null, }, }); From de1b140617ea4a6256e671d31f3279d0c3becc40 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Mon, 7 Jun 2021 18:27:54 +0300 Subject: [PATCH 08/28] chore(ci): fix ci conflict (#15016) --- .../src/filters/components/Select/SelectFilterPlugin.test.tsx | 3 +++ .../src/filters/components/Select/SelectFilterPlugin.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx index b29e64207be8b..e2ed21c90bc9b 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx @@ -200,6 +200,9 @@ describe('SelectFilterPlugin', () => { userEvent.click(screen.getByRole('combobox')); userEvent.click(screen.getByTitle('girl')); expect(setDataMask).toHaveBeenCalledWith({ + __cache: { + value: ['boy'], + }, extraFormData: { filters: [ { diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx index d10544a485035..305667c2947b5 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx @@ -51,7 +51,7 @@ type DataMaskAction = }; function reducer( - draft: Required & { __cache?: JsonObject }, + draft: DataMask & { __cache?: JsonObject }, action: DataMaskAction, ) { switch (action.type) { From 8798da41a7811ba87e33de6ba6cc3da6801ecbd4 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Mon, 7 Jun 2021 10:34:52 -0700 Subject: [PATCH 09/28] chore: rename 'Source' to 'Database' for consistency (#15021) --- superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx | 2 +- superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx index 008a39e6575f4..9ab653da5c04f 100644 --- a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx +++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx @@ -1070,7 +1070,7 @@ const AlertReportModal: FunctionComponent = ({
- {t('Source')} + {t('Database')} *
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx index fd18765bf0de5..7b1da36214720 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx @@ -275,7 +275,7 @@ const DatasetList: FunctionComponent = ({ size: 'md', }, { - Header: t('Source'), + Header: t('Database'), accessor: 'database.database_name', size: 'lg', }, From 7f4e03643329fccfba3cf67073efda1bf723c64d Mon Sep 17 00:00:00 2001 From: AAfghahi <48933336+AAfghahi@users.noreply.github.com> Date: Mon, 7 Jun 2021 14:10:28 -0400 Subject: [PATCH 10/28] fix: adding additional configs and colors for queryHistory (#14995) * added additional configs and colors for queryHistory * added condition to status icon * Update superset-frontend/src/SqlLab/components/QueryTable/index.jsx * Update superset-frontend/src/SqlLab/components/QueryTable/index.jsx --- .../SqlLab/components/QueryTable/index.jsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.jsx b/superset-frontend/src/SqlLab/components/QueryTable/index.jsx index b3bbed99cdba0..d23fd49ca6d24 100644 --- a/superset-frontend/src/SqlLab/components/QueryTable/index.jsx +++ b/superset-frontend/src/SqlLab/components/QueryTable/index.jsx @@ -88,6 +88,14 @@ const statusAttributes = { status: 'running', }, }, + fetching: { + color: ({ theme }) => theme.colors.primary.base, + config: { + name: 'queued', + label: t('fetching'), + status: 'fetching', + }, + }, timed_out: { color: ({ theme }) => theme.colors.grayscale.light1, config: { @@ -97,14 +105,20 @@ const statusAttributes = { }, }, scheduled: { - name: 'queued', - label: t('Scheduled'), - status: 'queued', + color: ({ theme }) => theme.colors.greyscale.base, + config: { + name: 'queued', + label: t('Scheduled'), + status: 'queued', + }, }, pending: { - name: 'queued', - label: t('Scheduled'), - status: 'queued', + color: ({ theme }) => theme.colors.greyscale.base, + config: { + name: 'queued', + label: t('Scheduled'), + status: 'queued', + }, }, }; From 422c32cb7de90ef7b7902d98a7e0102a25a01859 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Mon, 7 Jun 2021 22:49:56 +0300 Subject: [PATCH 11/28] feat(filter-box): hide druid options if druid not enabled (#14921) * feat(filter-box): hide druid options if druid not enabled * add bootstrap export --- .../visualizations/FilterBox/controlPanel.jsx | 67 ++++++++++++------- superset/views/base.py | 1 + 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/superset-frontend/src/visualizations/FilterBox/controlPanel.jsx b/superset-frontend/src/visualizations/FilterBox/controlPanel.jsx index 51c41d5c35ed3..34a814a43efe9 100644 --- a/superset-frontend/src/visualizations/FilterBox/controlPanel.jsx +++ b/superset-frontend/src/visualizations/FilterBox/controlPanel.jsx @@ -20,6 +20,36 @@ import React from 'react'; import { t } from '@superset-ui/core'; import { sections } from '@superset-ui/chart-controls'; +const appContainer = document.getElementById('app'); +const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); +const druidIsActive = !!bootstrapData?.common?.conf?.DRUID_IS_ACTIVE; +const druidSection = druidIsActive + ? [ + [ + { + name: 'show_druid_time_granularity', + config: { + type: 'CheckboxControl', + label: t('Show Druid granularity dropdown'), + default: false, + description: t('Check to include Druid granularity dropdown'), + }, + }, + ], + [ + { + name: 'show_druid_time_origin', + config: { + type: 'CheckboxControl', + label: t('Show Druid time origin'), + default: false, + description: t('Check to include time origin dropdown'), + }, + }, + ], + ] + : []; + export default { controlPanelSections: [ sections.legacyTimeseriesTime, @@ -51,6 +81,8 @@ export default { description: t('Whether to include a time filter'), }, }, + ], + [ { name: 'instant_filtering', config: { @@ -69,41 +101,30 @@ export default { name: 'show_sqla_time_granularity', config: { type: 'CheckboxControl', - label: t('Show SQL granularity dropdown'), + label: druidIsActive + ? t('Show SQL time grain dropdown') + : t('Show time grain dropdown'), default: false, - description: t('Check to include SQL granularity dropdown'), - }, - }, - { - name: 'show_sqla_time_column', - config: { - type: 'CheckboxControl', - label: t('Show SQL time column'), - default: false, - description: t('Check to include time column dropdown'), + description: druidIsActive + ? t('Check to include SQL time grain dropdown') + : t('Check to include time grain dropdown'), }, }, ], [ { - name: 'show_druid_time_granularity', - config: { - type: 'CheckboxControl', - label: t('Show Druid granularity dropdown'), - default: false, - description: t('Check to include Druid granularity dropdown'), - }, - }, - { - name: 'show_druid_time_origin', + name: 'show_sqla_time_column', config: { type: 'CheckboxControl', - label: t('Show Druid time origin'), + label: druidIsActive + ? t('Show SQL time column') + : t('Show time column'), default: false, - description: t('Check to include time origin dropdown'), + description: t('Check to include time column dropdown'), }, }, ], + ...druidSection, ['adhoc_filters'], ], }, diff --git a/superset/views/base.py b/superset/views/base.py index 74e05db5402aa..949406a999f2e 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -87,6 +87,7 @@ "SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT", "SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE", "DISABLE_DATASET_SOURCE_EDIT", + "DRUID_IS_ACTIVE", "ENABLE_JAVASCRIPT_CONTROLS", "DEFAULT_SQLLAB_LIMIT", "DEFAULT_VIZ_TYPE", From cf15fe0d03529432e1ef1741036ab5b60af197d2 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com> Date: Mon, 7 Jun 2021 14:04:56 -0700 Subject: [PATCH 12/28] fix(dashboard): custom css should be removed on unmount (#15025) * fix(dashboard): custom css should be removed on unmount * better comment * remove unnecessary typecast * move type to top level scope --- .../dashboard/containers/DashboardPage.tsx | 40 ++++++++----------- ...{injectCustomCss.js => injectCustomCss.ts} | 36 ++++++++++++----- 2 files changed, 43 insertions(+), 33 deletions(-) rename superset-frontend/src/dashboard/util/{injectCustomCss.js => injectCustomCss.ts} (66%) diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index 580df62b3e0dd..e5897d18c4647 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -26,7 +26,6 @@ import { useDashboardDatasets, } from 'src/common/hooks/apiResources'; import { ResourceStatus } from 'src/common/hooks/apiResources/apiResources'; -import { usePrevious } from 'src/common/hooks/usePrevious'; import { hydrateDashboard } from 'src/dashboard/actions/hydrate'; import injectCustomCss from 'src/dashboard/util/injectCustomCss'; @@ -42,14 +41,10 @@ const DashboardContainer = React.lazy( const DashboardPage: FC = () => { const dispatch = useDispatch(); const { idOrSlug } = useParams<{ idOrSlug: string }>(); - const [isLoaded, setLoaded] = useState(false); + const [isHydrated, setHydrated] = useState(false); const dashboardResource = useDashboard(idOrSlug); const chartsResource = useDashboardCharts(idOrSlug); const datasetsResource = useDashboardDatasets(idOrSlug); - const isLoading = [dashboardResource, chartsResource, datasetsResource].some( - resource => resource.status === ResourceStatus.LOADING, - ); - const wasLoading = usePrevious(isLoading); const error = [dashboardResource, chartsResource, datasetsResource].find( resource => resource.status === ResourceStatus.ERROR, )?.error; @@ -57,16 +52,22 @@ const DashboardPage: FC = () => { useEffect(() => { if (dashboardResource.result) { document.title = dashboardResource.result.dashboard_title; + if (dashboardResource.result.css) { + // returning will clean up custom css + // when dashboard unmounts or changes + return injectCustomCss(dashboardResource.result.css); + } } + return () => {}; }, [dashboardResource.result]); + const shouldBeHydrated = + dashboardResource.status === ResourceStatus.COMPLETE && + chartsResource.status === ResourceStatus.COMPLETE && + datasetsResource.status === ResourceStatus.COMPLETE; + useEffect(() => { - if ( - wasLoading && - dashboardResource.status === ResourceStatus.COMPLETE && - chartsResource.status === ResourceStatus.COMPLETE && - datasetsResource.status === ResourceStatus.COMPLETE - ) { + if (shouldBeHydrated) { dispatch( hydrateDashboard( dashboardResource.result, @@ -74,20 +75,13 @@ const DashboardPage: FC = () => { datasetsResource.result, ), ); - injectCustomCss(dashboardResource.result.css); - setLoaded(true); + setHydrated(true); } - }, [ - dispatch, - wasLoading, - dashboardResource, - chartsResource, - datasetsResource, - ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shouldBeHydrated]); if (error) throw error; // caught in error boundary - - if (!isLoaded) return ; + if (!isHydrated) return ; return ; }; diff --git a/superset-frontend/src/dashboard/util/injectCustomCss.js b/superset-frontend/src/dashboard/util/injectCustomCss.ts similarity index 66% rename from superset-frontend/src/dashboard/util/injectCustomCss.js rename to superset-frontend/src/dashboard/util/injectCustomCss.ts index 4f6238aad522a..b7db2b03d020a 100644 --- a/superset-frontend/src/dashboard/util/injectCustomCss.js +++ b/superset-frontend/src/dashboard/util/injectCustomCss.ts @@ -16,19 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -export default function injectCustomCss(css) { + +function createStyleElement(className: string) { + const style = document.createElement('style'); + style.className = className; + style.type = 'text/css'; + return style; +} + +// The original, non-typescript code referenced `style.styleSheet`. +// I can't find what sort of element would have a styleSheet property, +// so have created this type to satisfy TS without changing behavior. +type MysteryStyleElement = { + styleSheet: { + cssText: string; + }; +}; + +export default function injectCustomCss(css: string) { const className = 'CssEditor-css'; const head = document.head || document.getElementsByTagName('head')[0]; - let style = document.querySelector(`.${className}`); + const style: HTMLStyleElement = + document.querySelector(`.${className}`) || createStyleElement(className); - if (!style) { - style = document.createElement('style'); - style.className = className; - style.type = 'text/css'; - } - - if (style.styleSheet) { - style.styleSheet.cssText = css; + if ('styleSheet' in style) { + (style as MysteryStyleElement).styleSheet.cssText = css; } else { style.innerHTML = css; } @@ -45,4 +57,8 @@ export default function injectCustomCss(css) { */ head.appendChild(style); + + return function removeCustomCSS() { + style.remove(); + }; } From 11eef251b2348d1c895e70911ff4ad9d58e8cd19 Mon Sep 17 00:00:00 2001 From: Mikhail Kumachev Date: Tue, 8 Jun 2021 03:32:33 +0300 Subject: [PATCH 13/28] feat: Add "is_select_query" method to base engine spec to make it possible to override it (#15013) --- superset/db_engine_specs/base.py | 8 ++++++++ superset/sql_lab.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index 75d0953260c6e..3f123c1e394f7 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -1254,6 +1254,14 @@ def is_readonly_query(cls, parsed_query: ParsedQuery) -> bool: or parsed_query.is_show() ) + @classmethod + def is_select_query(cls, parsed_query: ParsedQuery) -> bool: + """ + Determine if the statement should be considered as SELECT statement. + Some query dialects do not contain "SELECT" word in queries (eg. Kusto) + """ + return parsed_query.is_select() + @classmethod @utils.memoized def get_column_spec( diff --git a/superset/sql_lab.py b/superset/sql_lab.py index 832962760bea5..30cec61c929bd 100644 --- a/superset/sql_lab.py +++ b/superset/sql_lab.py @@ -217,7 +217,7 @@ def execute_sql_statement( query.select_as_cta_used = True # Do not apply limit to the CTA queries when SQLLAB_CTAS_NO_LIMIT is set to true - if parsed_query.is_select() and not ( + if db_engine_spec.is_select_query(parsed_query) and not ( query.select_as_cta_used and SQLLAB_CTAS_NO_LIMIT ): if SQL_MAX_ROW and (not query.limit or query.limit > SQL_MAX_ROW): From 21aa3daae3e0f19fb354d67cee8777c41c908e8b Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Mon, 7 Jun 2021 19:12:56 -0700 Subject: [PATCH 14/28] display all metric results in editor (#15031) --- .../spec/fixtures/mockDatasource.js | 2 ++ .../datasource/DatasourceEditor_spec.jsx | 25 +++++++++++++++++++ .../src/datasource/DatasourceEditor.jsx | 13 +++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/superset-frontend/spec/fixtures/mockDatasource.js b/superset-frontend/spec/fixtures/mockDatasource.js index 99183edd5185b..e434ad73b0676 100644 --- a/superset-frontend/spec/fixtures/mockDatasource.js +++ b/superset-frontend/spec/fixtures/mockDatasource.js @@ -45,6 +45,8 @@ export default { verbose_name: 'sum__num', metric_name: 'sum__num', description: null, + extra: + '{"certification":{"details":"foo", "certified_by":"someone"},"warning_markdown":"bar"}', }, { expression: 'AVG(birth_names.num)', diff --git a/superset-frontend/spec/javascripts/datasource/DatasourceEditor_spec.jsx b/superset-frontend/spec/javascripts/datasource/DatasourceEditor_spec.jsx index 2ab5ea296bd59..39bcfad8658ec 100644 --- a/superset-frontend/spec/javascripts/datasource/DatasourceEditor_spec.jsx +++ b/superset-frontend/spec/javascripts/datasource/DatasourceEditor_spec.jsx @@ -21,6 +21,9 @@ import { shallow } from 'enzyme'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; import thunk from 'redux-thunk'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from 'spec/helpers/testing-library'; + import { Radio } from 'src/components/Radio'; import Icon from 'src/components/Icon'; @@ -56,6 +59,10 @@ describe('DatasourceEditor', () => { inst = wrapper.instance(); }); + afterEach(() => { + wrapper.unmount(); + }); + it('is valid', () => { expect(React.isValidElement(el)).toBe(true); }); @@ -209,3 +216,21 @@ describe('DatasourceEditor', () => { isFeatureEnabledMock.mockRestore(); }); }); + +describe('DatasourceEditor RTL', () => { + it('properly renders the metric information', async () => { + render(, { useRedux: true }); + const metricButton = screen.getByTestId('collection-tab-Metrics'); + userEvent.click(metricButton); + const expandToggle = await screen.findAllByLabelText(/toggle expand/i); + userEvent.click(expandToggle[0]); + const certificationDetails = await screen.findByPlaceholderText( + /certification details/i, + ); + expect(certificationDetails.value).toEqual('foo'); + const warningMarkdown = await await screen.findByPlaceholderText( + /certified by/i, + ); + expect(warningMarkdown.value).toEqual('someone'); + }); +}); diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx index 94d3cd976769f..3fb02364398cb 100644 --- a/superset-frontend/src/datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/datasource/DatasourceEditor.jsx @@ -936,7 +936,18 @@ class DatasourceEditor extends React.PureComponent { } - collection={this.state.datasource.metrics} + collection={this.state.datasource.metrics?.map(metric => { + const { + certification: { details, certified_by: certifiedBy } = {}, + warning_markdown: warningMarkdown, + } = JSON.parse(metric.extra || '{}') || {}; + return { + ...metric, + certification_details: details || '', + warning_markdown: warningMarkdown || '', + certified_by: certifiedBy, + }; + })} allowAddItem onChange={this.onDatasourcePropChange.bind(this, 'metrics')} itemGenerator={() => ({ From 12fcb3132c852eb094be518c541d304dd7849df2 Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Tue, 8 Jun 2021 07:50:05 +0300 Subject: [PATCH 15/28] Remove nowrap (#14954) --- superset-frontend/src/explore/components/ControlHeader.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/superset-frontend/src/explore/components/ControlHeader.jsx b/superset-frontend/src/explore/components/ControlHeader.jsx index 64cc7c205eeb4..c1b073459cf00 100644 --- a/superset-frontend/src/explore/components/ControlHeader.jsx +++ b/superset-frontend/src/explore/components/ControlHeader.jsx @@ -98,7 +98,6 @@ export default class ControlHeader extends React.Component { css={{ marginBottom: 0, position: 'relative', - whiteSpace: 'nowrap', }} > {this.props.leftNode && {this.props.leftNode}} From a1ca0b2e6bf3919f55a9cd91d1ace56b171b849a Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Tue, 8 Jun 2021 07:55:16 +0300 Subject: [PATCH 16/28] Add ming-height to empty tab (#14878) --- .../src/dashboard/components/gridComponents/Tabs.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx index c41abf83a4b08..f103653492f09 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx @@ -93,6 +93,10 @@ const StyledTabsContainer = styled.div` .ant-tabs { overflow: visible; + .ant-tabs-nav-wrap { + min-height: ${({ theme }) => theme.gridUnit * 12.5}px; + } + .ant-tabs-content-holder { overflow: visible; } From 0e07a5ca03cb2a6f560b77847c13413b9a8c7d97 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Tue, 8 Jun 2021 07:46:09 +0200 Subject: [PATCH 17/28] fix(explore): Datepicker glitch on hover outside the modal (#15033) --- .../controls/DateFilterControl/components/CustomFrame.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/CustomFrame.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/components/CustomFrame.tsx index fe486c3ef86d3..b9269a440487a 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/components/CustomFrame.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/CustomFrame.tsx @@ -130,7 +130,7 @@ export function CustomFrame(props: FrameComponentProps) { onChange('sinceDatetime', datetime.format(MOMENT_FORMAT)) } @@ -188,7 +188,7 @@ export function CustomFrame(props: FrameComponentProps) { onChange('untilDatetime', datetime.format(MOMENT_FORMAT)) } @@ -247,7 +247,7 @@ export function CustomFrame(props: FrameComponentProps) { onChange('anchorValue', datetime.format(MOMENT_FORMAT)) } From efd70077014cbed62e493372d33a2af5237eaadf Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Tue, 8 Jun 2021 14:55:11 +0300 Subject: [PATCH 18/28] fix(native-filters): show overridden chart name on scoping tree (#15038) --- .../FiltersConfigForm/FilterScope/utils.ts | 6 +++++- superset-frontend/src/dashboard/types.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts index 8c74aa7511c16..8cb48c1381553 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts @@ -45,7 +45,11 @@ export const buildTree = ( ) { const currentTreeItem = { key: node.id, - title: node.meta.sliceName || node.meta.text || node.id.toString(), + title: + node.meta.sliceNameOverride || + node.meta.sliceName || + node.meta.text || + node.id.toString(), children: [], }; treeItem.children.push(currentTreeItem); diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 9a15b1da7ef03..bd6eeedd27c98 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -93,6 +93,7 @@ export type LayoutItem = { chartId: number; height: number; sliceName?: string; + sliceNameOverride?: string; text?: string; uuid: string; width: number; From 1af91ed756515ebba9f200aff850564547070957 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Tue, 8 Jun 2021 09:58:15 -0300 Subject: [PATCH 19/28] fix: Adds left padding to dashboard edit mode when filter bar is closed (#15024) --- .../DashboardBuilder/DashboardBuilder.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 9873dfbd4ff9e..b38191c82f601 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -57,7 +57,10 @@ const HEADER_HEIGHT = 67; type DashboardBuilderProps = {}; -const StyledDashboardContent = styled.div<{ dashboardFiltersOpen: boolean }>` +const StyledDashboardContent = styled.div<{ + dashboardFiltersOpen: boolean; + editMode: boolean; +}>` display: flex; flex-direction: row; flex-wrap: nowrap; @@ -75,13 +78,15 @@ const StyledDashboardContent = styled.div<{ dashboardFiltersOpen: boolean }>` width: 100%; flex-grow: 1; position: relative; - margin: ${({ theme }) => theme.gridUnit * 6}px - ${({ theme }) => theme.gridUnit * 8}px - ${({ theme }) => theme.gridUnit * 6}px - ${({ theme, dashboardFiltersOpen }) => { - if (dashboardFiltersOpen) return theme.gridUnit * 8; + margin-top: ${({ theme }) => theme.gridUnit * 6}px; + margin-right: ${({ theme }) => theme.gridUnit * 8}px; + margin-bottom: ${({ theme }) => theme.gridUnit * 6}px; + margin-left: ${({ theme, dashboardFiltersOpen, editMode }) => { + if (!dashboardFiltersOpen && !editMode) { return 0; - }}px; + } + return theme.gridUnit * 8; + }}px; } .dashboard-component-chart-holder { @@ -204,6 +209,7 @@ const DashboardBuilder: FC = () => { {nativeFiltersEnabled && !editMode && ( Date: Tue, 8 Jun 2021 15:33:42 +0100 Subject: [PATCH 20/28] feat: add more timeout configuration on screenshots (#14868) * feat: more timeout configuration on screenshots * add tests --- superset/config.py | 10 ++++++-- superset/utils/webdriver.py | 37 ++++++++++++++--------------- tests/thumbnails_tests.py | 46 +++++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/superset/config.py b/superset/config.py index a6427b528d775..2d43fc18fa14f 100644 --- a/superset/config.py +++ b/superset/config.py @@ -475,11 +475,17 @@ def _try_json_readsha( # pylint: disable=unused-argument "CACHE_NO_NULL_WARNING": True, } -# Used for thumbnails and other api: Time in seconds before selenium +# Time in seconds before selenium # times out after trying to locate an element on the page and wait -# for that element to load for an alert screenshot. +# for that element to load for a screenshot. SCREENSHOT_LOCATE_WAIT = 10 +# Time in seconds before selenium +# times out after waiting for all DOM class elements named "loading" are gone. SCREENSHOT_LOAD_WAIT = 60 +# Selenium destroy retries +SCREENSHOT_SELENIUM_RETRIES = 5 +# Give selenium an headstart, in seconds +SCREENSHOT_SELENIUM_HEADSTART = 3 # --------------------------------------------------- # Image and file configuration diff --git a/superset/utils/webdriver.py b/superset/utils/webdriver.py index e7155ff12932b..70a4f512e3385 100644 --- a/superset/utils/webdriver.py +++ b/superset/utils/webdriver.py @@ -16,12 +16,16 @@ # under the License. import logging -import time +from time import sleep from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING from flask import current_app from retry.api import retry_call -from selenium.common.exceptions import TimeoutException, WebDriverException +from selenium.common.exceptions import ( + StaleElementReferenceException, + TimeoutException, + WebDriverException, +) from selenium.webdriver import chrome, firefox from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -33,11 +37,6 @@ WindowSize = Tuple[int, int] logger = logging.getLogger(__name__) -# Time in seconds, we will wait for the page to load and render -SELENIUM_CHECK_INTERVAL = 2 -SELENIUM_RETRIES = 5 -SELENIUM_HEADSTART = 3 - if TYPE_CHECKING: from flask_appbuilder.security.sqla.models import User @@ -95,18 +94,17 @@ def destroy(driver: WebDriver, tries: int = 2) -> None: pass def get_screenshot( - self, - url: str, - element_name: str, - user: "User", - retries: int = SELENIUM_RETRIES, + self, url: str, element_name: str, user: "User", ) -> Optional[bytes]: + driver = self.auth(user) driver.set_window_size(*self._window) driver.get(url) img: Optional[bytes] = None - logger.debug("Sleeping for %i seconds", SELENIUM_HEADSTART) - time.sleep(SELENIUM_HEADSTART) + selenium_headstart = current_app.config["SCREENSHOT_SELENIUM_HEADSTART"] + logger.debug("Sleeping for %i seconds", selenium_headstart) + sleep(selenium_headstart) + try: logger.debug("Wait for the presence of %s", element_name) element = WebDriverWait(driver, self._screenshot_locate_wait).until( @@ -120,11 +118,14 @@ def get_screenshot( img = element.screenshot_as_png except TimeoutException: logger.error("Selenium timed out requesting url %s", url, exc_info=True) + except StaleElementReferenceException: + logger.error( + "Selenium timed out while waiting for chart(s) to load %s", + url, + exc_info=True, + ) except WebDriverException as ex: logger.error(ex, exc_info=True) - # Some webdrivers do not support screenshots for elements. - # In such cases, take a screenshot of the entire page. - img = driver.screenshot() # pylint: disable=no-member finally: - self.destroy(driver, retries) + self.destroy(driver, current_app.config["SCREENSHOT_SELENIUM_RETRIES"]) return img diff --git a/tests/thumbnails_tests.py b/tests/thumbnails_tests.py index 4c0bd4ccbdf46..92d4f9993d4e7 100644 --- a/tests/thumbnails_tests.py +++ b/tests/thumbnails_tests.py @@ -19,7 +19,7 @@ import urllib.request from io import BytesIO from unittest import skipUnless -from unittest.mock import patch +from unittest.mock import ANY, call, patch from flask_testing import LiveServerTestCase from sqlalchemy.sql import func @@ -29,7 +29,8 @@ from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.utils.screenshots import ChartScreenshot, DashboardScreenshot -from superset.utils.urls import get_url_host +from superset.utils.urls import get_url_host, get_url_path +from superset.utils.webdriver import WebDriverProxy from tests.conftest import with_feature_flags from tests.test_app import app @@ -61,6 +62,47 @@ def test_get_async_dashboard_screenshot(self): self.assertEqual(response.getcode(), 202) +class TestWebDriverProxy(SupersetTestCase): + @patch("superset.utils.webdriver.WebDriverWait") + @patch("superset.utils.webdriver.firefox") + @patch("superset.utils.webdriver.sleep") + def test_screenshot_selenium_headstart( + self, mock_sleep, mock_webdriver, mock_webdriver_wait + ): + webdriver = WebDriverProxy("firefox") + user = security_manager.get_user_by_username( + app.config["THUMBNAIL_SELENIUM_USER"] + ) + url = get_url_path("Superset.slice", slice_id=1, standalone="true") + app.config["SCREENSHOT_SELENIUM_HEADSTART"] = 5 + webdriver.get_screenshot(url, "chart-container", user=user) + assert mock_sleep.call_args_list[0] == call(5) + + @patch("superset.utils.webdriver.WebDriverWait") + @patch("superset.utils.webdriver.firefox") + def test_screenshot_selenium_locate_wait(self, mock_webdriver, mock_webdriver_wait): + app.config["SCREENSHOT_LOCATE_WAIT"] = 15 + webdriver = WebDriverProxy("firefox") + user = security_manager.get_user_by_username( + app.config["THUMBNAIL_SELENIUM_USER"] + ) + url = get_url_path("Superset.slice", slice_id=1, standalone="true") + webdriver.get_screenshot(url, "chart-container", user=user) + assert mock_webdriver_wait.call_args_list[0] == call(ANY, 15) + + @patch("superset.utils.webdriver.WebDriverWait") + @patch("superset.utils.webdriver.firefox") + def test_screenshot_selenium_load_wait(self, mock_webdriver, mock_webdriver_wait): + app.config["SCREENSHOT_LOAD_WAIT"] = 15 + webdriver = WebDriverProxy("firefox") + user = security_manager.get_user_by_username( + app.config["THUMBNAIL_SELENIUM_USER"] + ) + url = get_url_path("Superset.slice", slice_id=1, standalone="true") + webdriver.get_screenshot(url, "chart-container", user=user) + assert mock_webdriver_wait.call_args_list[1] == call(ANY, 15) + + class TestThumbnails(SupersetTestCase): mock_image = b"bytes mock image" From 94c86c3837391c862b9df9471a3db783e7bd7061 Mon Sep 17 00:00:00 2001 From: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com> Date: Tue, 8 Jun 2021 12:09:35 -0500 Subject: [PATCH 21/28] Centered down-arrow icons in top navbar (#14846) --- superset-frontend/src/components/Menu/Menu.tsx | 3 +++ superset-frontend/src/components/Menu/MenuRight.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/superset-frontend/src/components/Menu/Menu.tsx b/superset-frontend/src/components/Menu/Menu.tsx index 0875b85f8a661..38c826b5ce4af 100644 --- a/superset-frontend/src/components/Menu/Menu.tsx +++ b/superset-frontend/src/components/Menu/Menu.tsx @@ -91,6 +91,9 @@ const StyledHeader = styled.header` flex-direction: column; justify-content: center; } + .main-nav .ant-menu-submenu-title > svg { + top: ${({ theme }) => theme.gridUnit * 5.25}px; + } @media (max-width: 767px) { .navbar-brand { float: none; diff --git a/superset-frontend/src/components/Menu/MenuRight.tsx b/superset-frontend/src/components/Menu/MenuRight.tsx index a722834bdf442..0507097343be4 100644 --- a/superset-frontend/src/components/Menu/MenuRight.tsx +++ b/superset-frontend/src/components/Menu/MenuRight.tsx @@ -59,6 +59,9 @@ const StyledDiv = styled.div<{ align: string }>` justify-content: ${({ align }) => align}; align-items: center; margin-right: ${({ theme }) => theme.gridUnit}px; + .ant-menu-submenu-title > svg { + top: ${({ theme }) => theme.gridUnit * 5.25}px; + } `; const StyledAnchor = styled.a` From 6cc179b7bf1b78f7883fd9bac6a944c980022b3f Mon Sep 17 00:00:00 2001 From: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com> Date: Tue, 8 Jun 2021 12:10:13 -0500 Subject: [PATCH 22/28] close icon aligned (#14870) --- superset-frontend/src/components/ListView/ListView.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index 8d9182ac70e4c..22729bd192e46 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -114,6 +114,10 @@ const BulkSelectWrapper = styled(Alert)` vertical-align: middle; position: relative; } + + .ant-alert-close-icon { + margin-top: ${({ theme }) => theme.gridUnit * 1.5}px; + } `; const bulkSelectColumnConfig = { From b75df937e984120525489dd5c0e7fbb4fcca5238 Mon Sep 17 00:00:00 2001 From: Ke Zhu Date: Tue, 8 Jun 2021 15:30:04 -0400 Subject: [PATCH 23/28] docs: provide config option for openid-connect provider (#15044) --- .../pages/docs/installation/configuring.mdx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/src/pages/docs/installation/configuring.mdx b/docs/src/pages/docs/installation/configuring.mdx index d4fc778f58896..daea083d2ead6 100644 --- a/docs/src/pages/docs/installation/configuring.mdx +++ b/docs/src/pages/docs/installation/configuring.mdx @@ -177,9 +177,28 @@ from custom_sso_security_manager import CustomSsoSecurityManager CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager ``` -Notice that the redirect URL will be `https:///oauth-authorized/` -When configuring an OAuth2 authorization provider if needed. For instance, the redirect URL will -be `https:///oauth-authorized/egaSSO` for the above configuration. +**Notes** + +- The redirect URL will be `https:///oauth-authorized/` + When configuring an OAuth2 authorization provider if needed. For instance, the redirect URL will + be `https:///oauth-authorized/egaSSO` for the above configuration. + +- If an OAuth2 authorization server supports OpenID Connect 1.0, you could configure its configuration + document URL only without providing `api_base_url`, `access_token_url`, `authorize_url` and other + required options like user info endpoint, jwks uri etc. For instance: + ```python + OAUTH_PROVIDERS = [ + { 'name':'egaSSO', + 'token_key':'access_token', # Name of the token in the response of access_token_url + 'icon':'fa-address-card', # Icon for the provider + 'remote_app': { + 'client_id':'myClientId', # Client Id (Identify Superset application) + 'client_secret':'MySecret', # Secret for this Client Id (Identify Superset application) + 'server_metadata_url': 'https://myAuthorizationServer/.well-known/openid-configuration' + } + } + ] + ``` ### Feature Flags From 3b97074ecbe686d59e22ec8b5434bc7c92f28088 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 8 Jun 2021 13:57:07 -0700 Subject: [PATCH 24/28] fix: benchmark migration script (#15032) --- scripts/benchmark_migration.py | 39 ++++++++++++------- .../27ae655e4247_make_creator_owners.py | 27 ++++++++++++- .../c82ee8a39623_add_implicit_tags.py | 36 +++++++++++++++-- superset/utils/mock_data.py | 6 ++- 4 files changed, 87 insertions(+), 21 deletions(-) diff --git a/scripts/benchmark_migration.py b/scripts/benchmark_migration.py index 0faa92a88552b..d226efbfd3058 100644 --- a/scripts/benchmark_migration.py +++ b/scripts/benchmark_migration.py @@ -25,10 +25,12 @@ from typing import Dict, List, Set, Type import click +from flask import current_app from flask_appbuilder import Model from flask_migrate import downgrade, upgrade from graphlib import TopologicalSorter # pylint: disable=wrong-import-order -from sqlalchemy import inspect +from sqlalchemy import create_engine, inspect, Table +from sqlalchemy.ext.automap import automap_base from superset import db from superset.utils.mock_data import add_sample_rows @@ -83,11 +85,18 @@ def find_models(module: ModuleType) -> List[Type[Model]]: elif isinstance(obj, dict): queue.extend(obj.values()) - # add implicit models - # pylint: disable=no-member, protected-access - for obj in Model._decl_class_registry.values(): - if hasattr(obj, "__table__") and obj.__table__.fullname in tables: - models.append(obj) + # build models by automapping the existing tables, instead of using current + # code; this is needed for migrations that modify schemas (eg, add a column), + # where the current model is out-of-sync with the existing table after a + # downgrade + sqlalchemy_uri = current_app.config["SQLALCHEMY_DATABASE_URI"] + engine = create_engine(sqlalchemy_uri) + Base = automap_base() + Base.prepare(engine, reflect=True) + for table in tables: + model = getattr(Base.classes, table) + model.__tablename__ = table + models.append(model) # sort topologically so we can create entities in order and # maintain relationships (eg, create a database before creating @@ -133,15 +142,6 @@ def main( ).scalar() print(f"Current version of the DB is {current_revision}") - print("\nIdentifying models used in the migration:") - models = find_models(module) - model_rows: Dict[Type[Model], int] = {} - for model in models: - rows = session.query(model).count() - print(f"- {model.__name__} ({rows} rows in table {model.__tablename__})") - model_rows[model] = rows - session.close() - if current_revision != down_revision: if not force: click.confirm( @@ -152,6 +152,15 @@ def main( ) downgrade(revision=down_revision) + print("\nIdentifying models used in the migration:") + models = find_models(module) + model_rows: Dict[Type[Model], int] = {} + for model in models: + rows = session.query(model).count() + print(f"- {model.__name__} ({rows} rows in table {model.__tablename__})") + model_rows[model] = rows + session.close() + print("Benchmarking migration") results: Dict[str, float] = {} start = time.time() diff --git a/superset/migrations/versions/27ae655e4247_make_creator_owners.py b/superset/migrations/versions/27ae655e4247_make_creator_owners.py index 561a8ca9a5440..c373c0f7e9090 100644 --- a/superset/migrations/versions/27ae655e4247_make_creator_owners.py +++ b/superset/migrations/versions/27ae655e4247_make_creator_owners.py @@ -27,10 +27,10 @@ down_revision = "d8bc074f7aad" from alembic import op +from flask import g from flask_appbuilder import Model -from flask_appbuilder.models.mixins import AuditMixin from sqlalchemy import Column, ForeignKey, Integer, Table -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.orm import relationship from superset import db @@ -62,6 +62,29 @@ class User(Base): ) +class AuditMixin: + @classmethod + def get_user_id(cls): + try: + return g.user.id + except Exception: + return None + + @declared_attr + def created_by_fk(cls): + return Column( + Integer, ForeignKey("ab_user.id"), default=cls.get_user_id, nullable=False + ) + + @declared_attr + def created_by(cls): + return relationship( + "User", + primaryjoin="%s.created_by_fk == User.id" % cls.__name__, + enable_typechecks=False, + ) + + class Slice(Base, AuditMixin): """Declarative class to do query in upgrade""" diff --git a/superset/migrations/versions/c82ee8a39623_add_implicit_tags.py b/superset/migrations/versions/c82ee8a39623_add_implicit_tags.py index 42fdb7e4668c2..3bab3f6ec3af9 100644 --- a/superset/migrations/versions/c82ee8a39623_add_implicit_tags.py +++ b/superset/migrations/versions/c82ee8a39623_add_implicit_tags.py @@ -26,16 +26,46 @@ revision = "c82ee8a39623" down_revision = "c617da68de7d" +from datetime import datetime + from alembic import op -from sqlalchemy import Column, Enum, ForeignKey, Integer, String -from sqlalchemy.ext.declarative import declarative_base +from flask_appbuilder.models.mixins import AuditMixin +from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String +from sqlalchemy.ext.declarative import declarative_base, declared_attr -from superset.models.helpers import AuditMixinNullable from superset.models.tags import ObjectTypes, TagTypes Base = declarative_base() +class AuditMixinNullable(AuditMixin): + """Altering the AuditMixin to use nullable fields + + Allows creating objects programmatically outside of CRUD + """ + + created_on = Column(DateTime, default=datetime.now, nullable=True) + changed_on = Column( + DateTime, default=datetime.now, onupdate=datetime.now, nullable=True + ) + + @declared_attr + def created_by_fk(self) -> Column: + return Column( + Integer, ForeignKey("ab_user.id"), default=self.get_user_id, nullable=True, + ) + + @declared_attr + def changed_by_fk(self) -> Column: + return Column( + Integer, + ForeignKey("ab_user.id"), + default=self.get_user_id, + onupdate=self.get_user_id, + nullable=True, + ) + + class Tag(Base, AuditMixinNullable): """A tag attached to an object (query, chart or dashboard).""" diff --git a/superset/utils/mock_data.py b/superset/utils/mock_data.py index 06327ef89262b..84981ca59b8e3 100644 --- a/superset/utils/mock_data.py +++ b/superset/utils/mock_data.py @@ -29,6 +29,7 @@ import sqlalchemy_utils from flask_appbuilder import Model from sqlalchemy import Column, inspect, MetaData, Table +from sqlalchemy.dialects import postgresql from sqlalchemy.orm import Session from sqlalchemy.sql import func from sqlalchemy.sql.visitors import VisitableType @@ -146,6 +147,9 @@ def get_type_generator(sqltype: sqlalchemy.sql.sqltypes) -> Callable[[], Any]: if isinstance(sqltype, sqlalchemy_utils.types.uuid.UUIDType): return uuid4 + if isinstance(sqltype, postgresql.base.UUID): + return lambda: str(uuid4()) + if isinstance(sqltype, sqlalchemy.sql.sqltypes.BLOB): length = random.randrange(sqltype.length or 255) return lambda: os.urandom(length) @@ -153,7 +157,7 @@ def get_type_generator(sqltype: sqlalchemy.sql.sqltypes) -> Callable[[], Any]: logger.warning( "Unknown type %s. Please add it to `get_type_generator`.", type(sqltype) ) - return lambda: "UNKNOWN TYPE" + return lambda: b"UNKNOWN TYPE" def add_data( From a59bbbc544da1221017388e712d56ed539525628 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 8 Jun 2021 15:04:04 -0700 Subject: [PATCH 25/28] fix: edit BQ w/o encrypted_extra (#15048) * fix: edit BQ w/o encrypted_extra * Fix lint --- superset/models/core.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/superset/models/core.py b/superset/models/core.py index ee8e822477a8f..4173377e06490 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -239,15 +239,12 @@ def backend(self) -> str: @property def parameters(self) -> Dict[str, Any]: - # Build parameters if db_engine_spec is a subclass of BasicParametersMixin - parameters = {"engine": self.backend} - - if hasattr(self.db_engine_spec, "parameters_schema") and hasattr( - self.db_engine_spec, "get_parameters_from_uri" - ): - uri = make_url(self.sqlalchemy_uri_decrypted) - encrypted_extra = self.get_encrypted_extra() - return {**parameters, **self.db_engine_spec.get_parameters_from_uri(uri, encrypted_extra=encrypted_extra)} # type: ignore + uri = make_url(self.sqlalchemy_uri_decrypted) + encrypted_extra = self.get_encrypted_extra() + try: + parameters = self.db_engine_spec.get_parameters_from_uri(uri, encrypted_extra=encrypted_extra) # type: ignore + except Exception: # pylint: disable=broad-except + parameters = {} return parameters From 3f527c7a45967b540bdad16028b8b96e744fcee3 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 8 Jun 2021 15:52:57 -0700 Subject: [PATCH 26/28] fix: font regression in SQL Lab (#14960) * fix: font regression in SQL Lab * Fix tests --- .../components/ErrorMessage/DatabaseErrorMessage.test.tsx | 1 + .../src/components/ErrorMessage/DatabaseErrorMessage.tsx | 3 ++- .../ErrorMessage/ErrorMessageWithStackTrace.tsx | 8 +++++++- .../ErrorMessage/ParameterErrorMessage.test.tsx | 1 + .../src/components/ErrorMessage/ParameterErrorMessage.tsx | 3 ++- superset-frontend/src/components/ErrorMessage/types.ts | 1 + 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx index 0b23866352142..6959c5351a7ee 100644 --- a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx @@ -44,6 +44,7 @@ const mockedProps = { message: 'Error message', }, source: 'dashboard' as ErrorSource, + subtitle: 'Error message', }; test('should render', () => { diff --git a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx index 0454aca59ef0f..5798f3b4106fc 100644 --- a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx +++ b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx @@ -35,6 +35,7 @@ interface DatabaseErrorExtra { function DatabaseErrorMessage({ error, source = 'dashboard', + subtitle, }: ErrorMessageComponentProps) { const { extra, level, message } = error; @@ -81,7 +82,7 @@ ${extra.issue_codes.map(issueCode => issueCode.message).join('\n')}`; return ( ; + return ( + + ); } } diff --git a/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx index d4664d53c62e6..17f38c4d23c57 100644 --- a/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx @@ -44,6 +44,7 @@ const mockedProps = { message: 'Error message', }, source: 'dashboard' as ErrorSource, + subtitle: 'Error message', }; test('should render', () => { diff --git a/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.tsx b/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.tsx index 1eb44371ed7e6..666ad32169ade 100644 --- a/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.tsx +++ b/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.tsx @@ -54,6 +54,7 @@ const findMatches = (undefinedParameters: string[], templateKeys: string[]) => { function ParameterErrorMessage({ error, source = 'sqllab', + subtitle, }: ErrorMessageComponentProps) { const { extra, level, message } = error; @@ -112,7 +113,7 @@ ${extra.issue_codes.map(issueCode => issueCode.message).join('\n')}`; return ( = { error: SupersetError; source?: ErrorSource; + subtitle?: React.ReactNode; }; export type ErrorMessageComponent = React.ComponentType; From 42cb5266fa9f5a6ae40221029cef7a54c559a51d Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 8 Jun 2021 18:56:55 -0700 Subject: [PATCH 27/28] fix: import metrics with extra (#15047) * fix: import metrics with extra * Fix test --- superset/datasets/commands/importers/v1/utils.py | 3 ++- tests/datasets/commands_tests.py | 2 +- tests/fixtures/importexport.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/superset/datasets/commands/importers/v1/utils.py b/superset/datasets/commands/importers/v1/utils.py index c21c66ff18077..6b4dbeb7b8e74 100644 --- a/superset/datasets/commands/importers/v1/utils.py +++ b/superset/datasets/commands/importers/v1/utils.py @@ -98,11 +98,12 @@ def import_dataset( except TypeError: logger.info("Unable to encode `%s` field: %s", key, config[key]) for metric in config.get("metrics", []): - if metric.get("extra"): + if metric.get("extra") is not None: try: metric["extra"] = json.dumps(metric["extra"]) except TypeError: logger.info("Unable to encode `extra` field: %s", metric["extra"]) + metric["extra"] = None # should we delete columns and metrics not present in the current import? sync = ["columns", "metrics"] if overwrite else [] diff --git a/tests/datasets/commands_tests.py b/tests/datasets/commands_tests.py index 4df7365a5527f..1293a301ff93b 100644 --- a/tests/datasets/commands_tests.py +++ b/tests/datasets/commands_tests.py @@ -328,7 +328,7 @@ def test_import_v1_dataset(self): assert metric.expression == "count(1)" assert metric.description is None assert metric.d3format is None - assert metric.extra is None + assert metric.extra == "{}" assert metric.warning_text is None assert len(dataset.columns) == 1 diff --git a/tests/fixtures/importexport.py b/tests/fixtures/importexport.py index 04aaaa3945cff..951ecf9bb4350 100644 --- a/tests/fixtures/importexport.py +++ b/tests/fixtures/importexport.py @@ -384,7 +384,7 @@ "expression": "count(1)", "description": None, "d3format": None, - "extra": None, + "extra": {}, "warning_text": None, }, ], From a9eda01764e962c1d6c6f002208a97bfb4e6a251 Mon Sep 17 00:00:00 2001 From: "Hugh A. Miles II" Date: Tue, 8 Jun 2021 22:28:20 -0400 Subject: [PATCH 28/28] feat(database-connection-ui) Allow configuration of Database Images from SupersetText (#15023) * saving this for now * fix some styles * add database images * fix * enforce only numbers * add default iamge --- .../images/icons/default_db_image.svg | 22 +++++++++++++++++++ .../Form/LabeledErrorBoundInput.tsx | 5 +++++ .../src/components/Icon/index.tsx | 3 +++ .../src/components/IconButton/index.tsx | 19 +++++++++++----- .../DatabaseModal/DatabaseConnectionForm.tsx | 1 + .../data/database/DatabaseModal/index.tsx | 6 +++-- superset-frontend/src/views/CRUD/hooks.ts | 3 +++ 7 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 superset-frontend/images/icons/default_db_image.svg diff --git a/superset-frontend/images/icons/default_db_image.svg b/superset-frontend/images/icons/default_db_image.svg new file mode 100644 index 0000000000000..3cd24f4888d8c --- /dev/null +++ b/superset-frontend/images/icons/default_db_image.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx index 75df2bb088cbb..f71df9d18ebae 100644 --- a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx +++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx @@ -62,6 +62,11 @@ const alertIconStyles = (theme: SupersetTheme, hasError: boolean) => css` }`} `; const StyledFormGroup = styled('div')` + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } margin-bottom: ${({ theme }) => theme.gridUnit * 5}px; .ant-form-item { margin-bottom: 0; diff --git a/superset-frontend/src/components/Icon/index.tsx b/superset-frontend/src/components/Icon/index.tsx index 693354d9a7367..e83dde1131381 100644 --- a/superset-frontend/src/components/Icon/index.tsx +++ b/superset-frontend/src/components/Icon/index.tsx @@ -145,6 +145,7 @@ import { ReactComponent as WarningIcon } from 'images/icons/warning.svg'; import { ReactComponent as WarningSolidIcon } from 'images/icons/warning_solid.svg'; import { ReactComponent as XLargeIcon } from 'images/icons/x-large.svg'; import { ReactComponent as XSmallIcon } from 'images/icons/x-small.svg'; +import { ReactComponent as DefaultDatabaseIcon } from 'images/icons/default_db_image.svg'; export type IconName = | 'alert' @@ -184,6 +185,7 @@ export type IconName = | 'copy' | 'cursor-target' | 'database' + | 'default-database' | 'dataset-physical' | 'dataset-virtual' | 'dataset-virtual-greyscale' @@ -299,6 +301,7 @@ export const iconsRegistry: Record< 'circle-check-solid': CircleCheckSolidIcon, 'color-palette': ColorPaletteIcon, 'cursor-target': CursorTargeIcon, + 'default-database': DefaultDatabaseIcon, 'dataset-physical': DatasetPhysicalIcon, 'dataset-virtual': DatasetVirtualIcon, 'dataset-virtual-greyscale': DatasetVirtualGreyscaleIcon, diff --git a/superset-frontend/src/components/IconButton/index.tsx b/superset-frontend/src/components/IconButton/index.tsx index fb10b77a4a2a3..164d93e67d86f 100644 --- a/superset-frontend/src/components/IconButton/index.tsx +++ b/superset-frontend/src/components/IconButton/index.tsx @@ -17,9 +17,10 @@ * under the License. */ import React from 'react'; -import { styled } from '@superset-ui/core'; +import { styled, supersetTheme } from '@superset-ui/core'; import Button from 'src/components/Button'; import { ButtonProps as AntdButtonProps } from 'antd/lib/button'; +import Icon from 'src/components/Icon'; export interface IconButtonProps extends AntdButtonProps { buttonText: string; @@ -35,7 +36,6 @@ const StyledButton = styled(Button)` width: 33%; `; const StyledImage = styled.div` - margin: ${({ theme }) => theme.gridUnit * 8}px 0; padding: ${({ theme }) => theme.gridUnit * 4}px; &:first-of-type { @@ -43,7 +43,8 @@ const StyledImage = styled.div` } img { - width: fit-content; + width: 100%; + height: 100%; &:first-of-type { margin-right: 0; @@ -82,7 +83,6 @@ const StyledBottom = styled.div` background-color: ${({ theme }) => theme.colors.grayscale.light4}; width: 100%; line-height: 1.5em; - overflow: hidden; white-space: no-wrap; text-overflow: ellipsis; @@ -95,8 +95,17 @@ const IconButton = styled( ({ icon, altText, buttonText, ...props }: IconButtonProps) => ( - {altText} + {icon && {altText}} + {!icon && ( + + )} + {buttonText} diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx index 1eac5de10e057..f70f10565eb4f 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx @@ -176,6 +176,7 @@ const portField = ({ = ({ const [dbName, setDbName] = useState(''); const [isLoading, setLoading] = useState(false); const conf = useCommonConf(); + const dbImages = getDatabaseImages(); const isEditMode = !!databaseId; const sslForced = isFeatureEnabled( FeatureFlag.FORCE_DATABASE_CONNECTIONS_SSL, @@ -336,7 +338,6 @@ const DatabaseModal: FunctionComponent = ({ Or choose from a list of other databases we support{' '} -