You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2023/08/04 17:13:33 UTC

[superset] branch 3.0 updated (571f33536e -> e46f10a8a6)

This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a change to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git


    from 571f33536e chore: Updates CHANGELOG.md
     new b89d7387a2 fix: pass schema on dataset creation (#24815)
     new 7d2da96e81 fix: Removes unnecessary query on filters (#24814)
     new 1a0d270e5b fix(migration): Ensure cascadeParentIds key exists (#24831)
     new a9b8c8e3ec fix: Allow chart import to update the dataset an existing chart points to (#24821)
     new b5df3f9e4e fix(datasets): give possibility to add dataset with slashes in name (#24796)
     new 161e05445c fix: Python3.11 (str, Enum) issue (#24803)
     new bbe4e016d8 fix(embedded): adding logic to check dataset used by filters (#24808)
     new 34adeb4f4f fix: Links in tooltips of dashboard chart cards (#24846)
     new 29528e9783 fix(legacy-chart): corrupted raw chart data (#24850)
     new 21ded992ad fix(sqllab): Add docText for long keyword (#24847)
     new e47377e576 fix: Explore misleading save action (#24862)
     new 8cf702bf3d fix: validation errors appearing after ssh tunnel switch (#24849)
     new 21764f9ae3 fix(annotation): Address regression from #24694 (#24874)
     new 84035badab fix(explore): invalid "No Filter" applied (#24876)
     new b272814ff5 fix(dataset): resizable dataset layout left column (#24829)
     new e46f10a8a6 chore: add talisman env var to config (#24774)

The 16 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .pylintrc                                          |   1 +
 .../src/query/types/AnnotationLayer.ts             |   3 +-
 .../test/query/types/AnnotationLayer.test.ts       |   1 -
 .../legacy-preset-chart-nvd3/src/transformProps.js |   1 +
 .../test/utils/annotation.test.ts                  |   1 -
 .../AceEditorWrapper/useKeywords.test.ts           |  46 +++++
 .../components/AceEditorWrapper/useKeywords.ts     |   9 +
 .../SaveDatasetModal/SaveDatasetModal.test.tsx     |  42 ++++-
 .../SqlLab/components/SaveDatasetModal/index.tsx   |   1 +
 superset-frontend/src/SqlLab/fixtures.ts           |   1 +
 .../src/components/AsyncAceEditor/index.tsx        |   2 +
 .../components/AddSliceCard/AddSliceCard.test.tsx  |  21 ++-
 .../components/AddSliceCard/AddSliceCard.tsx       |  26 ++-
 .../src/explore/actions/saveModalActions.js        |   9 +-
 .../src/explore/components/SaveModal.test.jsx      |  55 +++---
 .../src/explore/components/SaveModal.tsx           | 205 +++++++++++----------
 .../src/explore/reducers/saveModalReducer.js       |   3 -
 .../DatabaseConnectionForm/CommonParameters.tsx    |   3 +
 .../DatabaseModal/DatabaseConnectionForm/index.tsx |   4 +
 .../src/features/databases/DatabaseModal/index.tsx |   2 +-
 .../datasets/AddDataset/LeftPanel/index.tsx        |   1 -
 .../src/features/datasets/DatasetLayout/index.tsx  |  20 +-
 superset-frontend/src/features/datasets/styles.ts  |  21 +--
 .../components/Select/SelectFilterPlugin.test.tsx  |  47 +++--
 .../components/Select/SelectFilterPlugin.tsx       |  17 +-
 superset/charts/commands/importers/v1/utils.py     |   4 +-
 superset/common/chart_data.py                      |   6 +-
 superset/common/db_query_status.py                 |   4 +-
 superset/config.py                                 |   4 +-
 superset/connectors/base/models.py                 |   4 +-
 superset/constants.py                              |   8 +-
 superset/databases/api.py                          |   8 +-
 superset/errors.py                                 |   7 +-
 superset/key_value/types.py                        |   6 +-
 ...6f8b1280_cleanup_erroneous_parent_filter_ids.py |   3 +-
 superset/models/core.py                            |   6 +-
 superset/models/helpers.py                         |  14 +-
 superset/reports/models.py                         |  17 +-
 superset/security/manager.py                       |  25 ++-
 superset/sql_parse.py                              |   6 +-
 superset/sqllab/limiting_factor.py                 |   4 +-
 superset/tasks/types.py                            |   5 +-
 .../command_status.py => utils/backports.py}       |  12 +-
 superset/utils/core.py                             |  31 ++--
 superset/views/core.py                             |   4 +-
 tests/integration_tests/databases/api_tests.py     |  16 ++
 .../security/guest_token_security_tests.py         |  46 +++++
 tests/unit_tests/utils/test_core.py                |  39 ++++
 tests/unit_tests/utils/test_decorators.py          |   4 +-
 49 files changed, 555 insertions(+), 270 deletions(-)
 copy superset/{sqllab/command_status.py => utils/backports.py} (82%)


[superset] 12/16: fix: validation errors appearing after ssh tunnel switch (#24849)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 8cf702bf3dd3f357dc6cfd4d8e162d4ba09f15e9
Author: Hugh A. Miles II <hu...@gmail.com>
AuthorDate: Wed Aug 2 17:41:37 2023 -0400

    fix: validation errors appearing after ssh tunnel switch (#24849)
    
    (cherry picked from commit b71541fb7fb1bdfd3e1eea59ee76de1f51e67e6b)
---
 .../DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx         | 3 +++
 .../features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx | 4 ++++
 superset-frontend/src/features/databases/DatabaseModal/index.tsx      | 2 +-
 3 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
index 7078b49cbc..7b52eab26c 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
@@ -49,6 +49,7 @@ export const hostField = ({
     onChange={changeMethods.onParametersChange}
   />
 );
+
 export const portField = ({
   required,
   changeMethods,
@@ -255,6 +256,7 @@ export const forceSSLField = ({
 export const SSHTunnelSwitch = ({
   isEditMode,
   changeMethods,
+  clearValidationErrors,
   db,
 }: FieldPropTypes) => (
   <div css={(theme: SupersetTheme) => infoTooltip(theme)}>
@@ -270,6 +272,7 @@ export const SSHTunnelSwitch = ({
             value: changed,
           },
         });
+        clearValidationErrors();
       }}
       data-test="ssh-tunnel-switch"
     />
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
index 5dce73206f..e747b3c895 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
@@ -79,6 +79,7 @@ export interface FieldPropTypes {
   };
   validationErrors: JsonObject | null;
   getValidation: () => void;
+  clearValidationErrors: () => void;
   db?: DatabaseObject;
   field: string;
   isEditMode?: boolean;
@@ -132,6 +133,7 @@ interface DatabaseConnectionFormProps {
   onRemoveTableCatalog: (idx: number) => void;
   validationErrors: JsonObject | null;
   getValidation: () => void;
+  clearValidationErrors: () => void;
   getPlaceholder?: (field: string) => string | undefined;
 }
 
@@ -151,6 +153,7 @@ const DatabaseConnectionForm = ({
   onRemoveTableCatalog,
   sslForced,
   validationErrors,
+  clearValidationErrors,
 }: DatabaseConnectionFormProps) => (
   <Form>
     <div
@@ -179,6 +182,7 @@ const DatabaseConnectionForm = ({
             },
             validationErrors,
             getValidation,
+            clearValidationErrors,
             db,
             key: field,
             field,
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
index 1861cc46ed..dd2e405350 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
@@ -267,7 +267,6 @@ export function dbReducer(
       };
     case ActionType.extraInputChange:
       // "extra" payload in state is a string
-
       if (
         action.payload.name === 'schema_cache_timeout' ||
         action.payload.name === 'table_cache_timeout'
@@ -1635,6 +1634,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
         getValidation={() => getValidation(db)}
         validationErrors={validationErrors}
         getPlaceholder={getPlaceholder}
+        clearValidationErrors={() => setValidationErrors(null)}
       />
       {db?.parameters?.ssh && (
         <SSHTunnelContainer>{renderSSHTunnelForm()}</SSHTunnelContainer>


[superset] 09/16: fix(legacy-chart): corrupted raw chart data (#24850)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 29528e9783c99bc59ee4b70d0dbf139a62971e54
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Tue Aug 1 09:49:04 2023 -0700

    fix(legacy-chart): corrupted raw chart data (#24850)
    
    (cherry picked from commit 1c5971d3afb70a338444c41943ff90c3a9c03ec3)
---
 superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js
index cf54d4c9f7..4287cd64c4 100644
--- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js
+++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js
@@ -111,6 +111,7 @@ export default function transformProps(chartProps) {
   const data = Array.isArray(rawData)
     ? rawData.map(row => ({
         ...row,
+        values: row.values.map(value => ({ ...value })),
         key: formatLabel(row.key, datasource.verboseMap),
       }))
     : rawData;


[superset] 08/16: fix: Links in tooltips of dashboard chart cards (#24846)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 34adeb4f4f25c613d9ddc030489de667a3f3149d
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Tue Aug 1 10:28:10 2023 -0300

    fix: Links in tooltips of dashboard chart cards (#24846)
    
    (cherry picked from commit ea17dd637c9259236292d7d81887e59f0f14eacc)
---
 .../components/AddSliceCard/AddSliceCard.test.tsx  | 21 ++++++++++++++++-
 .../components/AddSliceCard/AddSliceCard.tsx       | 26 +++++++++++++++++-----
 2 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.test.tsx b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.test.tsx
index 26cd7b945d..3aa22a8408 100644
--- a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.test.tsx
+++ b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.test.tsx
@@ -19,7 +19,8 @@
 
 import React from 'react';
 import { FeatureFlag } from '@superset-ui/core';
-import { act, render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { act, render, screen, within } from 'spec/helpers/testing-library';
 import AddSliceCard from '.';
 
 jest.mock('src/components/DynamicPlugins', () => ({
@@ -60,3 +61,21 @@ test('render thumbnail if feature flag is set', async () => {
 
   expect(screen.queryByTestId('thumbnail')).toBeInTheDocument();
 });
+
+test('does not render the tooltip with anchors', async () => {
+  const mock = jest
+    .spyOn(React, 'useState')
+    .mockImplementation(() => [true, jest.fn()]);
+  render(
+    <AddSliceCard
+      {...mockedProps}
+      datasourceUrl="http://test.com"
+      datasourceName="datasource-name"
+    />,
+  );
+  userEvent.hover(screen.getByRole('link', { name: 'datasource-name' }));
+  expect(await screen.findByRole('tooltip')).toBeInTheDocument();
+  const tooltip = await screen.findByRole('tooltip');
+  expect(within(tooltip).queryByRole('link')).not.toBeInTheDocument();
+  mock.mockRestore();
+});
diff --git a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
index bf008a359e..af824bf4c3 100644
--- a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
+++ b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
@@ -24,6 +24,7 @@ import React, {
   useMemo,
   useRef,
   useState,
+  PropsWithChildren,
 } from 'react';
 import { t, isFeatureEnabled, FeatureFlag, css } from '@superset-ui/core';
 import ImageLoader from 'src/components/ListViewCard/ImageLoader';
@@ -34,8 +35,15 @@ import { Theme } from '@emotion/react';
 
 const FALLBACK_THUMBNAIL_URL = '/static/assets/images/chart-card-fallback.svg';
 
-const TruncatedTextWithTooltip: React.FC = ({ children, ...props }) => {
-  const [isTruncated, setIsTruncated] = useState(false);
+const TruncatedTextWithTooltip = ({
+  children,
+  tooltipText,
+  ...props
+}: PropsWithChildren<{
+  tooltipText?: string;
+}>) => {
+  // Uses React.useState for testing purposes
+  const [isTruncated, setIsTruncated] = React.useState(false);
   const ref = useRef<HTMLDivElement>(null);
   useEffect(() => {
     setIsTruncated(
@@ -58,13 +66,18 @@ const TruncatedTextWithTooltip: React.FC = ({ children, ...props }) => {
     </div>
   );
 
-  return isTruncated ? <Tooltip title={children}>{div}</Tooltip> : div;
+  return isTruncated ? (
+    <Tooltip title={tooltipText || children}>{div}</Tooltip>
+  ) : (
+    div
+  );
 };
 
 const MetadataItem: React.FC<{
   label: ReactNode;
   value: ReactNode;
-}> = ({ label, value }) => (
+  tooltipText?: string;
+}> = ({ label, value, tooltipText }) => (
   <div
     css={(theme: Theme) => css`
       font-size: ${theme.typography.sizes.s}px;
@@ -89,7 +102,9 @@ const MetadataItem: React.FC<{
         min-width: 0;
       `}
     >
-      <TruncatedTextWithTooltip>{value}</TruncatedTextWithTooltip>
+      <TruncatedTextWithTooltip tooltipText={tooltipText}>
+        {value}
+      </TruncatedTextWithTooltip>
     </span>
   </div>
 );
@@ -273,6 +288,7 @@ const AddSliceCard: React.FC<{
                     datasourceName
                   )
                 }
+                tooltipText={datasourceName}
               />
               <MetadataItem label={t('Modified')} value={lastModified} />
             </div>


[superset] 16/16: chore: add talisman env var to config (#24774)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit e46f10a8a6d4e569ed178f845d2a119b9df40aae
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Fri Aug 4 09:04:14 2023 -0700

    chore: add talisman env var to config (#24774)
    
    (cherry picked from commit d23b20ea75457f35efbd25ca6b36f594e47eef91)
---
 superset/config.py                  |  4 +++-
 superset/utils/core.py              |  2 ++
 tests/unit_tests/utils/test_core.py | 39 +++++++++++++++++++++++++++++++++++++
 3 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/superset/config.py b/superset/config.py
index f7f0d3ed81..ded4dc1404 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -53,6 +53,7 @@ from superset.key_value.types import JsonKeyValueCodec
 from superset.stats_logger import DummyStatsLogger
 from superset.superset_typing import CacheConfig
 from superset.tasks.types import ExecutorType
+from superset.utils import core as utils
 from superset.utils.core import is_test, NO_TIME_RANGE, parse_boolean_string
 from superset.utils.encrypt import SQLAlchemyUtilsAdapter
 from superset.utils.log import DBEventLogger
@@ -1381,7 +1382,8 @@ TEST_DATABASE_CONNECTION_TIMEOUT = timedelta(seconds=30)
 CONTENT_SECURITY_POLICY_WARNING = True
 
 # Do you want Talisman enabled?
-TALISMAN_ENABLED = True
+TALISMAN_ENABLED = utils.cast_to_boolean(os.environ.get("TALISMAN_ENABLED", True))
+
 # If you want Talisman, how do you want it configured??
 TALISMAN_CONFIG = {
     "content_security_policy": {
diff --git a/superset/utils/core.py b/superset/utils/core.py
index f40ccb63a2..58df8b3c0f 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -439,6 +439,8 @@ def cast_to_boolean(value: Any) -> bool | None:
     """
     if value is None:
         return None
+    if isinstance(value, bool):
+        return value
     if isinstance(value, (int, float)):
         return value != 0
     if isinstance(value, str):
diff --git a/tests/unit_tests/utils/test_core.py b/tests/unit_tests/utils/test_core.py
index 996bd1948f..568595517c 100644
--- a/tests/unit_tests/utils/test_core.py
+++ b/tests/unit_tests/utils/test_core.py
@@ -20,6 +20,7 @@ from typing import Any, Optional
 import pytest
 
 from superset.utils.core import (
+    cast_to_boolean,
     is_test,
     parse_boolean_string,
     QueryObjectFilterClause,
@@ -132,3 +133,41 @@ def test_is_test():
 )
 def test_parse_boolean_string(test_input: Optional[str], expected: bool):
     assert parse_boolean_string(test_input) == expected
+
+
+def test_int_values():
+    assert cast_to_boolean(1) is True
+    assert cast_to_boolean(0) is False
+    assert cast_to_boolean(-1) is True
+    assert cast_to_boolean(42) is True
+    assert cast_to_boolean(0) is False
+
+
+def test_float_values():
+    assert cast_to_boolean(0.5) is True
+    assert cast_to_boolean(3.14) is True
+    assert cast_to_boolean(-2.71) is True
+    assert cast_to_boolean(0.0) is False
+
+
+def test_string_values():
+    assert cast_to_boolean("true") is True
+    assert cast_to_boolean("TruE") is True
+    assert cast_to_boolean("false") is False
+    assert cast_to_boolean("FaLsE") is False
+    assert cast_to_boolean("") is False
+
+
+def test_none_value():
+    assert cast_to_boolean(None) is None
+
+
+def test_boolean_values():
+    assert cast_to_boolean(True) is True
+    assert cast_to_boolean(False) is False
+
+
+def test_other_values():
+    assert cast_to_boolean([]) is False
+    assert cast_to_boolean({}) is False
+    assert cast_to_boolean(object()) is False


[superset] 15/16: fix(dataset): resizable dataset layout left column (#24829)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b272814ff527bd063e5e7037ca51e0bb96b10cb5
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Thu Aug 3 09:34:33 2023 -0700

    fix(dataset): resizable dataset layout left column (#24829)
    
    (cherry picked from commit 6ff7fae0b006f7ec7d8a04011b0d46506ea139c6)
---
 .../datasets/AddDataset/LeftPanel/index.tsx         |  1 -
 .../src/features/datasets/DatasetLayout/index.tsx   | 20 +++++++++++++++++---
 superset-frontend/src/features/datasets/styles.ts   | 21 ++++++++-------------
 3 files changed, 25 insertions(+), 17 deletions(-)

diff --git a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
index 715bf2deee..1be10f4a05 100644
--- a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
@@ -37,7 +37,6 @@ interface LeftPanelProps {
 
 const LeftPanelStyle = styled.div`
   ${({ theme }) => `
-    max-width: ${theme.gridUnit * 87.5}px;
     padding: ${theme.gridUnit * 4}px;
     height: 100%;
     background-color: ${theme.colors.grayscale.light5};
diff --git a/superset-frontend/src/features/datasets/DatasetLayout/index.tsx b/superset-frontend/src/features/datasets/DatasetLayout/index.tsx
index d264fab061..ccf05aede8 100644
--- a/superset-frontend/src/features/datasets/DatasetLayout/index.tsx
+++ b/superset-frontend/src/features/datasets/DatasetLayout/index.tsx
@@ -17,6 +17,9 @@
  * under the License.
  */
 import React, { ReactElement, JSXElementConstructor } from 'react';
+import { useTheme } from '@superset-ui/core';
+import ResizableSidebar from 'src/components/ResizableSidebar';
+
 import {
   StyledLayoutWrapper,
   LeftColumn,
@@ -46,14 +49,25 @@ export default function DatasetLayout({
   rightPanel,
   footer,
 }: DatasetLayoutProps) {
+  const theme = useTheme();
+
   return (
     <StyledLayoutWrapper data-test="dataset-layout-wrapper">
       {header && <StyledLayoutHeader>{header}</StyledLayoutHeader>}
       <OuterRow>
         {leftPanel && (
-          <LeftColumn>
-            <StyledLayoutLeftPanel>{leftPanel}</StyledLayoutLeftPanel>
-          </LeftColumn>
+          <ResizableSidebar
+            id="dataset"
+            initialWidth={theme.gridUnit * 80}
+            minWidth={theme.gridUnit * 80}
+            enable
+          >
+            {adjustedWidth => (
+              <LeftColumn width={adjustedWidth}>
+                <StyledLayoutLeftPanel>{leftPanel}</StyledLayoutLeftPanel>
+              </LeftColumn>
+            )}
+          </ResizableSidebar>
         )}
         <RightColumn>
           <PanelRow>
diff --git a/superset-frontend/src/features/datasets/styles.ts b/superset-frontend/src/features/datasets/styles.ts
index 268b9e273e..728aa12ae4 100644
--- a/superset-frontend/src/features/datasets/styles.ts
+++ b/superset-frontend/src/features/datasets/styles.ts
@@ -25,22 +25,17 @@ export const StyledLayoutWrapper = styled.div`
   background-color: ${({ theme }) => theme.colors.grayscale.light5};
 `;
 
-const Column = styled.div`
-  width: 100%;
-  height: 100%;
+export const LeftColumn = styled.div<{ width?: number }>`
+  width: ${({ theme, width }) => width ?? theme.gridUnit * 80}px;
+  max-width: ${({ theme, width }) => width ?? theme.gridUnit * 80}px;
   flex-direction: column;
+  flex: 1 0 auto;
 `;
 
-export const LeftColumn = styled(Column)`
-  width: ${({ theme }) => theme.gridUnit * 80}px;
-  height: auto;
-`;
-
-export const RightColumn = styled(Column)`
-  height: auto;
+export const RightColumn = styled.div`
   display: flex;
-  flex: 1 0 auto;
-  width: calc(100% - ${({ theme }) => theme.gridUnit * 80}px);
+  flex-direction: column;
+  flex-grow: 1;
 `;
 
 const Row = styled.div`
@@ -52,6 +47,7 @@ const Row = styled.div`
 
 export const OuterRow = styled(Row)`
   flex: 1 0 auto;
+  position: relative;
 `;
 
 export const PanelRow = styled(Row)`
@@ -87,7 +83,6 @@ export const StyledCreateDatasetTitle = styled.div`
 
 export const StyledLayoutLeftPanel = styled.div`
   ${({ theme }) => `
-  width: ${theme.gridUnit * 80}px;
   height: 100%;
   border-right: 1px solid ${theme.colors.grayscale.light2};
   `}


[superset] 02/16: fix: Removes unnecessary query on filters (#24814)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 7d2da96e81fa4a13bca2a1cd8c0c5f0eb7fe69a2
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Fri Jul 28 10:56:51 2023 -0300

    fix: Removes unnecessary query on filters (#24814)
    
    (cherry picked from commit 5bb8e0da897e62382807cc374d2cc3b5a6e2de03)
---
 .../components/Select/SelectFilterPlugin.test.tsx  | 47 +++++++++++-----------
 .../components/Select/SelectFilterPlugin.tsx       | 17 +-------
 2 files changed, 24 insertions(+), 40 deletions(-)

diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
index f0aa4453b1..c035f81c01 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
@@ -235,34 +235,33 @@ describe('SelectFilterPlugin', () => {
     });
   });
 
-  test('Add ownState with column types when search all options', async () => {
+  test('receives the correct filter when search all options', async () => {
     getWrapper({ searchAllOptions: true, multiSelect: false });
     userEvent.click(screen.getByRole('combobox'));
     expect(await screen.findByRole('combobox')).toBeInTheDocument();
     userEvent.click(screen.getByTitle('girl'));
-    expect(setDataMask).toHaveBeenCalledWith({
-      __cache: {
-        value: ['boy'],
-      },
-      extraFormData: {
-        filters: [
-          {
-            col: 'gender',
-            op: 'IN',
-            val: ['girl'],
-          },
-        ],
-      },
-      filterState: {
-        label: 'girl',
-        value: ['girl'],
-      },
-      ownState: {
-        coltypeMap: {
-          gender: 1,
+    expect(setDataMask).toHaveBeenLastCalledWith(
+      expect.objectContaining({
+        extraFormData: {
+          filters: [
+            {
+              col: 'gender',
+              op: 'IN',
+              val: ['girl'],
+            },
+          ],
         },
-        search: null,
-      },
-    });
+      }),
+    );
+  });
+  test('number of fired queries when searching', async () => {
+    getWrapper({ searchAllOptions: true });
+    userEvent.click(screen.getByRole('combobox'));
+    expect(await screen.findByRole('combobox')).toBeInTheDocument();
+    await userEvent.type(screen.getByRole('combobox'), 'a');
+    // Closes the select
+    userEvent.tab();
+    // One call for the search term and other for the empty search
+    expect(setDataMask).toHaveBeenCalledTimes(2);
   });
 });
diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
index 0ebcb03a5d..2c5d919188 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
@@ -185,23 +185,9 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
     [dispatchDataMask, initialColtypeMap, searchAllOptions],
   );
 
-  const clearSuggestionSearch = useCallback(() => {
-    setSearch('');
-    if (searchAllOptions) {
-      dispatchDataMask({
-        type: 'ownState',
-        ownState: {
-          coltypeMap: initialColtypeMap,
-          search: null,
-        },
-      });
-    }
-  }, [dispatchDataMask, initialColtypeMap, searchAllOptions]);
-
   const handleBlur = useCallback(() => {
-    clearSuggestionSearch();
     unsetFocusedFilter();
-  }, [clearSuggestionSearch, unsetFocusedFilter]);
+  }, [unsetFocusedFilter]);
 
   const handleChange = useCallback(
     (value?: SelectValue | number | string) => {
@@ -323,7 +309,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
           mode={multiSelect ? 'multiple' : 'single'}
           placeholder={placeholderText}
           onSearch={onSearch}
-          onSelect={clearSuggestionSearch}
           onBlur={handleBlur}
           onFocus={setFocusedFilter}
           onMouseEnter={setHoveredFilter}


[superset] 11/16: fix: Explore misleading save action (#24862)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit e47377e5764ddb5b9af8e621a0c70afe45ce1a0b
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Wed Aug 2 15:46:03 2023 -0300

    fix: Explore misleading save action (#24862)
    
    (cherry picked from commit bf1b1a4c46c4de6ed4f0f576fc459d0d5e94e6f3)
---
 .../src/explore/actions/saveModalActions.js        |   5 -
 .../src/explore/components/SaveModal.test.jsx      |  55 +++---
 .../src/explore/components/SaveModal.tsx           | 205 +++++++++++----------
 .../src/explore/reducers/saveModalReducer.js       |   3 -
 4 files changed, 141 insertions(+), 127 deletions(-)

diff --git a/superset-frontend/src/explore/actions/saveModalActions.js b/superset-frontend/src/explore/actions/saveModalActions.js
index de4f7af84c..1a9970ab75 100644
--- a/superset-frontend/src/explore/actions/saveModalActions.js
+++ b/superset-frontend/src/explore/actions/saveModalActions.js
@@ -49,11 +49,6 @@ export function saveSliceSuccess(data) {
   return { type: SAVE_SLICE_SUCCESS, data };
 }
 
-export const REMOVE_SAVE_MODAL_ALERT = 'REMOVE_SAVE_MODAL_ALERT';
-export function removeSaveModalAlert() {
-  return { type: REMOVE_SAVE_MODAL_ALERT };
-}
-
 const extractAddHocFiltersFromFormData = formDataToHandle =>
   Object.entries(formDataToHandle).reduce(
     (acc, [key, value]) =>
diff --git a/superset-frontend/src/explore/components/SaveModal.test.jsx b/superset-frontend/src/explore/components/SaveModal.test.jsx
index d9d6f2983d..bdb93e5429 100644
--- a/superset-frontend/src/explore/components/SaveModal.test.jsx
+++ b/superset-frontend/src/explore/components/SaveModal.test.jsx
@@ -24,7 +24,6 @@ import { bindActionCreators } from 'redux';
 import { shallow } from 'enzyme';
 import { Radio } from 'src/components/Radio';
 import Button from 'src/components/Button';
-import sinon from 'sinon';
 import fetchMock from 'fetch-mock';
 
 import * as saveModalActions from 'src/explore/actions/saveModalActions';
@@ -131,7 +130,7 @@ test('renders a Modal with the right set of components', () => {
   expect(footerWrapper.find(Button)).toHaveLength(3);
 });
 
-test('renders the right footer buttons when existing dashboard selected', () => {
+test('renders the right footer buttons', () => {
   const wrapper = getWrapper();
   const footerWrapper = shallow(wrapper.find(StyledModal).props().footer);
   const saveAndGoDash = footerWrapper
@@ -142,18 +141,43 @@ test('renders the right footer buttons when existing dashboard selected', () =>
   expect(saveAndGoDash.props.children).toBe('Save & go to dashboard');
 });
 
-test('renders the right footer buttons when new dashboard selected', () => {
+test('does not render a message when overriding', () => {
+  const wrapper = getWrapper();
+  wrapper.setState({
+    action: 'overwrite',
+  });
+  expect(
+    wrapper.find('[message="A new chart will be created."]'),
+  ).not.toExist();
+});
+
+test('renders a message when saving as', () => {
+  const wrapper = getWrapper();
+  wrapper.setState({
+    action: 'saveas',
+  });
+  expect(wrapper.find('[message="A new chart will be created."]')).toExist();
+});
+
+test('renders a message when a new dashboard is selected', () => {
   const wrapper = getWrapper();
   wrapper.setState({
     dashboard: { label: 'Test new dashboard', value: 'Test new dashboard' },
   });
-  const footerWrapper = shallow(wrapper.find(StyledModal).props().footer);
-  const saveAndGoDash = footerWrapper
-    .find('#btn_modal_save_goto_dash')
-    .getElement();
-  const save = footerWrapper.find('#btn_modal_save').getElement();
-  expect(save.props.children).toBe('Save to new dashboard');
-  expect(saveAndGoDash.props.children).toBe('Save & go to new dashboard');
+  expect(
+    wrapper.find('[message="A new dashboard will be created."]'),
+  ).toExist();
+});
+
+test('renders a message when saving as with new dashboard', () => {
+  const wrapper = getWrapper();
+  wrapper.setState({
+    action: 'saveas',
+    dashboard: { label: 'Test new dashboard', value: 'Test new dashboard' },
+  });
+  expect(
+    wrapper.find('[message="A new chart and dashboard will be created."]'),
+  ).toExist();
 });
 
 test('disables overwrite option for new slice', () => {
@@ -197,17 +221,6 @@ test('updates slice name and selected dashboard', () => {
   expect(wrapper.state().dashboard.value).toBe(dashboardId);
 });
 
-test('removes alert', () => {
-  sinon.spy(defaultProps.actions, 'removeSaveModalAlert');
-  const wrapper = getWrapper();
-  wrapper.setProps({ alert: 'old alert' });
-
-  wrapper.instance().removeAlert();
-  expect(defaultProps.actions.removeSaveModalAlert.callCount).toBe(1);
-  expect(wrapper.state().alert).toBeNull();
-  defaultProps.actions.removeSaveModalAlert.restore();
-});
-
 test('set dataset name when chart source is query', () => {
   const wrapper = getWrapper(queryDefaultProps, queryStore);
   expect(wrapper.find('[data-test="new-dataset-name"]')).toExist();
diff --git a/superset-frontend/src/explore/components/SaveModal.tsx b/superset-frontend/src/explore/components/SaveModal.tsx
index 592bab2f9f..3142d6b758 100644
--- a/superset-frontend/src/explore/components/SaveModal.tsx
+++ b/superset-frontend/src/explore/components/SaveModal.tsx
@@ -67,7 +67,6 @@ interface SaveModalProps extends RouteComponentProps {
 type SaveModalState = {
   newSliceName?: string;
   datasetName: string;
-  alert: string | null;
   action: SaveActionType;
   isLoading: boolean;
   saveStatus?: string | null;
@@ -92,7 +91,6 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
     this.state = {
       newSliceName: props.sliceName,
       datasetName: props.datasource?.name,
-      alert: null,
       action: this.canOverwriteSlice() ? 'overwrite' : 'saveas',
       isLoading: false,
       vizType: props.form_data?.viz_type,
@@ -103,7 +101,6 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
     this.changeAction = this.changeAction.bind(this);
     this.saveOrOverwrite = this.saveOrOverwrite.bind(this);
     this.isNewDashboard = this.isNewDashboard.bind(this);
-    this.removeAlert = this.removeAlert.bind(this);
     this.onHide = this.onHide.bind(this);
   }
 
@@ -163,8 +160,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
   }
 
   async saveOrOverwrite(gotodash: boolean) {
-    this.setState({ alert: null, isLoading: true });
-    this.props.actions.removeSaveModalAlert();
+    this.setState({ isLoading: true });
 
     //  Create or retrieve dashboard
     type DashboardGetResponse = {
@@ -324,89 +320,115 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
     };
   };
 
-  renderSaveChartModal = () => (
-    <Form data-test="save-modal-body" layout="vertical">
-      {(this.state.alert || this.props.alert) && (
-        <Alert
-          type="warning"
-          message={this.state.alert || this.props.alert}
-          onClose={this.removeAlert}
-        />
-      )}
-      <FormItem data-test="radio-group">
-        <Radio
-          id="overwrite-radio"
-          disabled={!this.canOverwriteSlice()}
-          checked={this.state.action === 'overwrite'}
-          onChange={() => this.changeAction('overwrite')}
-          data-test="save-overwrite-radio"
-        >
-          {t('Save (Overwrite)')}
-        </Radio>
-        <Radio
-          id="saveas-radio"
-          data-test="saveas-radio"
-          checked={this.state.action === 'saveas'}
-          onChange={() => this.changeAction('saveas')}
-        >
-          {t('Save as...')}
-        </Radio>
-      </FormItem>
-      <hr />
-      <FormItem label={t('Chart name')} required>
-        <Input
-          name="new_slice_name"
-          type="text"
-          placeholder="Name"
-          value={this.state.newSliceName}
-          onChange={this.onSliceNameChange}
-          data-test="new-chart-name"
-        />
-      </FormItem>
-      {this.props.datasource?.type === 'query' && (
-        <FormItem label={t('Dataset Name')} required>
-          <InfoTooltipWithTrigger
-            tooltip={t('A reusable dataset will be saved with your chart.')}
-            placement="right"
-          />
+  renderSaveChartModal = () => {
+    const info = this.info();
+    return (
+      <Form data-test="save-modal-body" layout="vertical">
+        <FormItem data-test="radio-group">
+          <Radio
+            id="overwrite-radio"
+            disabled={!this.canOverwriteSlice()}
+            checked={this.state.action === 'overwrite'}
+            onChange={() => this.changeAction('overwrite')}
+            data-test="save-overwrite-radio"
+          >
+            {t('Save (Overwrite)')}
+          </Radio>
+          <Radio
+            id="saveas-radio"
+            data-test="saveas-radio"
+            checked={this.state.action === 'saveas'}
+            onChange={() => this.changeAction('saveas')}
+          >
+            {t('Save as...')}
+          </Radio>
+        </FormItem>
+        <hr />
+        <FormItem label={t('Chart name')} required>
           <Input
-            name="dataset_name"
+            name="new_slice_name"
             type="text"
-            placeholder="Dataset Name"
-            value={this.state.datasetName}
-            onChange={this.handleDatasetNameChange}
-            data-test="new-dataset-name"
+            placeholder="Name"
+            value={this.state.newSliceName}
+            onChange={this.onSliceNameChange}
+            data-test="new-chart-name"
           />
         </FormItem>
-      )}
-      {!(
-        isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
-        this.state.vizType === 'filter_box'
-      ) && (
-        <FormItem
-          label={t('Add to dashboard')}
-          data-test="save-chart-modal-select-dashboard-form"
-        >
-          <AsyncSelect
-            allowClear
-            allowNewOptions
-            ariaLabel={t('Select a dashboard')}
-            options={this.loadDashboards}
-            onChange={this.onDashboardChange}
-            value={this.state.dashboard}
-            placeholder={
-              <div>
-                <b>{t('Select')}</b>
-                {t(' a dashboard OR ')}
-                <b>{t('create')}</b>
-                {t(' a new one')}
-              </div>
-            }
+        {this.props.datasource?.type === 'query' && (
+          <FormItem label={t('Dataset Name')} required>
+            <InfoTooltipWithTrigger
+              tooltip={t('A reusable dataset will be saved with your chart.')}
+              placement="right"
+            />
+            <Input
+              name="dataset_name"
+              type="text"
+              placeholder="Dataset Name"
+              value={this.state.datasetName}
+              onChange={this.handleDatasetNameChange}
+              data-test="new-dataset-name"
+            />
+          </FormItem>
+        )}
+        {!(
+          isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
+          this.state.vizType === 'filter_box'
+        ) && (
+          <FormItem
+            label={t('Add to dashboard')}
+            data-test="save-chart-modal-select-dashboard-form"
+          >
+            <AsyncSelect
+              allowClear
+              allowNewOptions
+              ariaLabel={t('Select a dashboard')}
+              options={this.loadDashboards}
+              onChange={this.onDashboardChange}
+              value={this.state.dashboard}
+              placeholder={
+                <div>
+                  <b>{t('Select')}</b>
+                  {t(' a dashboard OR ')}
+                  <b>{t('create')}</b>
+                  {t(' a new one')}
+                </div>
+              }
+            />
+          </FormItem>
+        )}
+        {info && <Alert type="info" message={info} closable={false} />}
+        {this.props.alert && (
+          <Alert
+            css={{ marginTop: info ? 16 : undefined }}
+            type="warning"
+            message={this.props.alert}
+            closable={false}
           />
-        </FormItem>
-      )}
-    </Form>
-  );
+        )}
+      </Form>
+    );
+  };
+
+  info = () => {
+    const isNewDashboard = this.isNewDashboard();
+    let chartWillBeCreated = false;
+    if (
+      this.props.slice &&
+      (this.state.action !== 'overwrite' || !this.canOverwriteSlice())
+    ) {
+      chartWillBeCreated = true;
+    }
+    if (chartWillBeCreated && isNewDashboard) {
+      return t('A new chart and dashboard will be created.');
+    }
+    if (chartWillBeCreated) {
+      return t('A new chart will be created.');
+    }
+    if (isNewDashboard) {
+      return t('A new dashboard will be created.');
+    }
+    return null;
+  };
 
   renderFooter = () => (
     <div data-test="save-modal-footer">
@@ -426,9 +448,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
         }
         onClick={() => this.saveOrOverwrite(true)}
       >
-        {this.isNewDashboard()
-          ? t('Save & go to new dashboard')
-          : t('Save & go to dashboard')}
+        {t('Save & go to dashboard')}
       </Button>
       <Button
         id="btn_modal_save"
@@ -443,22 +463,11 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
         }
         data-test="btn-modal-save"
       >
-        {!this.canOverwriteSlice() && this.props.slice
-          ? t('Save as new chart')
-          : this.isNewDashboard()
-          ? t('Save to new dashboard')
-          : t('Save')}
+        {t('Save')}
       </Button>
     </div>
   );
 
-  removeAlert() {
-    if (this.props.alert) {
-      this.props.actions.removeSaveModalAlert();
-    }
-    this.setState({ alert: null });
-  }
-
   render() {
     return (
       <StyledModal
diff --git a/superset-frontend/src/explore/reducers/saveModalReducer.js b/superset-frontend/src/explore/reducers/saveModalReducer.js
index 85983e1ef1..572cabde39 100644
--- a/superset-frontend/src/explore/reducers/saveModalReducer.js
+++ b/superset-frontend/src/explore/reducers/saveModalReducer.js
@@ -40,9 +40,6 @@ export default function saveModalReducer(state = {}, action) {
     [actions.SAVE_SLICE_SUCCESS](data) {
       return { ...state, data };
     },
-    [actions.REMOVE_SAVE_MODAL_ALERT]() {
-      return { ...state, saveModalAlert: null };
-    },
     [HYDRATE_EXPLORE]() {
       return { ...action.data.saveModal };
     },


[superset] 03/16: fix(migration): Ensure cascadeParentIds key exists (#24831)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 1a0d270e5ba8dde70d306e7e1f9945759f579b49
Author: John Bodley <45...@users.noreply.github.com>
AuthorDate: Fri Jul 28 13:54:18 2023 -0700

    fix(migration): Ensure cascadeParentIds key exists (#24831)
    
    (cherry picked from commit caffe3cb1f971519d29268ad136bb07abe6b59f9)
---
 ...023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/superset/migrations/versions/2023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py b/superset/migrations/versions/2023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py
index 17ad592b22..f5bdb29a96 100644
--- a/superset/migrations/versions/2023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py
+++ b/superset/migrations/versions/2023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py
@@ -61,14 +61,13 @@ def upgrade():
                     filter_ids = {fltr["id"] for fltr in filters}
 
                     for fltr in filters:
-                        for parent_id in fltr["cascadeParentIds"][:]:
+                        for parent_id in fltr.get("cascadeParentIds", [])[:]:
                             if parent_id not in filter_ids:
                                 fltr["cascadeParentIds"].remove(parent_id)
                                 updated = True
 
                 if updated:
                     dashboard.json_metadata = json.dumps(json_metadata)
-
             except Exception:
                 logging.exception(
                     f"Unable to parse JSON metadata for dashboard {dashboard.id}"


[superset] 10/16: fix(sqllab): Add docText for long keyword (#24847)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 21ded992ad4d76ce3ebeeb4c61f024134744c293
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Wed Aug 2 09:02:57 2023 -0700

    fix(sqllab): Add docText for long keyword (#24847)
    
    (cherry picked from commit 1a9c559a8f6c1e0cf59ac1d102ac42fba3458f8c)
---
 .../AceEditorWrapper/useKeywords.test.ts           | 46 ++++++++++++++++++++++
 .../components/AceEditorWrapper/useKeywords.ts     |  9 +++++
 .../src/components/AsyncAceEditor/index.tsx        |  2 +
 3 files changed, 57 insertions(+)

diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
index 6a7c79b85b..12bd95b402 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
@@ -267,3 +267,49 @@ test('returns column keywords among selected tables', async () => {
     ),
   );
 });
+
+test('returns long keywords with docText', async () => {
+  const expectLongKeywordDbId = 2;
+  const longKeyword = 'veryveryveryveryverylongtablename';
+  const dbFunctionNamesApiRoute = `glob:*/api/v1/database/${expectLongKeywordDbId}/function_names/`;
+  fetchMock.get(dbFunctionNamesApiRoute, { function_names: [] });
+
+  act(() => {
+    store.dispatch(
+      schemaApiUtil.upsertQueryData(
+        'schemas',
+        {
+          dbId: expectLongKeywordDbId,
+          forceRefresh: false,
+        },
+        ['short', longKeyword].map(value => ({
+          value,
+          label: value,
+          title: value,
+        })),
+      ),
+    );
+  });
+  const { result, waitFor } = renderHook(
+    () =>
+      useKeywords({
+        queryEditorId: 'testqueryid',
+        dbId: expectLongKeywordDbId,
+      }),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+  await waitFor(() =>
+    expect(result.current).toContainEqual(
+      expect.objectContaining({
+        name: longKeyword,
+        value: longKeyword,
+        docText: longKeyword,
+      }),
+    ),
+  );
+});
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
index e7582158f4..b42edc5822 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
@@ -50,6 +50,11 @@ const EMPTY_LIST = [] as typeof sqlKeywords;
 const { useQueryState: useSchemasQueryState } = schemaEndpoints.schemas;
 const { useQueryState: useTablesQueryState } = tableEndpoints.tables;
 
+const getHelperText = (value: string) =>
+  value.length > 30 && {
+    docText: value,
+  };
+
 export function useKeywords(
   { queryEditorId, dbId, schema }: Params,
   skip = false,
@@ -149,6 +154,7 @@ export function useKeywords(
         completer: {
           insertMatch,
         },
+        ...getHelperText(s.value),
       })),
     [schemaOptions, insertMatch],
   );
@@ -163,6 +169,7 @@ export function useKeywords(
         completer: {
           insertMatch,
         },
+        ...getHelperText(value),
       })),
     [tableData?.options, insertMatch],
   );
@@ -174,6 +181,7 @@ export function useKeywords(
         value: col,
         score: COLUMN_AUTOCOMPLETE_SCORE,
         meta: 'column',
+        ...getHelperText(col),
       })),
     [allColumns],
   );
@@ -188,6 +196,7 @@ export function useKeywords(
         completer: {
           insertMatch,
         },
+        ...getHelperText(func),
       })),
     [functionNames, insertMatch],
   );
diff --git a/superset-frontend/src/components/AsyncAceEditor/index.tsx b/superset-frontend/src/components/AsyncAceEditor/index.tsx
index e4fa51f56b..2e499e150b 100644
--- a/superset-frontend/src/components/AsyncAceEditor/index.tsx
+++ b/superset-frontend/src/components/AsyncAceEditor/index.tsx
@@ -35,6 +35,8 @@ export interface AceCompleterKeywordData {
   value: string;
   score: number;
   meta: string;
+  docText?: string;
+  docHTML?: string;
 }
 
 export type TextMode = OrigTextMode & { $id: string };


[superset] 05/16: fix(datasets): give possibility to add dataset with slashes in name (#24796)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b5df3f9e4ea75e1ad5c25da5aff5ff256a155ebf
Author: Stepan <66...@users.noreply.github.com>
AuthorDate: Fri Jul 28 20:33:08 2023 +0300

    fix(datasets): give possibility to add dataset with slashes in name (#24796)
---
 superset/databases/api.py                      |  8 ++++----
 superset/views/core.py                         |  4 +++-
 tests/integration_tests/databases/api_tests.py | 16 ++++++++++++++++
 3 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/superset/databases/api.py b/superset/databases/api.py
index 98932e28e2..caa46d3164 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -672,7 +672,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         except DatabaseTablesUnexpectedError as ex:
             return self.response_422(ex.message)
 
-    @expose("/<int:pk>/table/<table_name>/<schema_name>/", methods=("GET",))
+    @expose("/<int:pk>/table/<path:table_name>/<schema_name>/", methods=("GET",))
     @protect()
     @check_datasource_access
     @safe
@@ -735,7 +735,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         self.incr_stats("success", self.table_metadata.__name__)
         return self.response(200, **table_info)
 
-    @expose("/<int:pk>/table_extra/<table_name>/<schema_name>/", methods=("GET",))
+    @expose("/<int:pk>/table_extra/<path:table_name>/<schema_name>/", methods=("GET",))
     @protect()
     @check_datasource_access
     @safe
@@ -798,8 +798,8 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         )
         return self.response(200, **payload)
 
-    @expose("/<int:pk>/select_star/<table_name>/", methods=("GET",))
-    @expose("/<int:pk>/select_star/<table_name>/<schema_name>/", methods=("GET",))
+    @expose("/<int:pk>/select_star/<path:table_name>/", methods=("GET",))
+    @expose("/<int:pk>/select_star/<path:table_name>/<schema_name>/", methods=("GET",))
     @protect()
     @check_datasource_access
     @safe
diff --git a/superset/views/core.py b/superset/views/core.py
index f9bf51d54c..cf1d8db08c 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -938,7 +938,9 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
     @has_access
     @event_logger.log_this
     @expose("/fetch_datasource_metadata")
-    @deprecated(new_target="api/v1/database/<int:pk>/table/<table_name>/<schema_name>/")
+    @deprecated(
+        new_target="api/v1/database/<int:pk>/table/<path:table_name>/<schema_name>/"
+    )
     def fetch_datasource_metadata(self) -> FlaskResponse:  # pylint: disable=no-self-use
         """
         Fetch the datasource metadata.
diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py
index 568ba05934..846f0020de 100644
--- a/tests/integration_tests/databases/api_tests.py
+++ b/tests/integration_tests/databases/api_tests.py
@@ -684,6 +684,22 @@ class TestDatabaseApi(SupersetTestCase):
         # the DB should not be created
         assert model is None
 
+    def test_get_table_details_with_slash_in_table_name(self):
+        table_name = "table_with/slash"
+        database = get_example_database()
+        query = f'CREATE TABLE IF NOT EXISTS "{table_name}" (col VARCHAR(256))'
+        if database.backend == "mysql":
+            query = query.replace('"', "`")
+
+        with database.get_sqla_engine_with_context() as engine:
+            engine.execute(query)
+
+        self.login(username="admin")
+        uri = f"api/v1/database/{database.id}/table/{table_name}/null/"
+        rv = self.client.get(uri)
+
+        self.assertEqual(rv.status_code, 200)
+
     def test_create_database_invalid_configuration_method(self):
         """
         Database API: Test create with an invalid configuration method.


[superset] 14/16: fix(explore): invalid "No Filter" applied (#24876)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 84035badab4a189ac25ab40f5254e9e1d26d5d95
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Thu Aug 3 06:17:56 2023 -0700

    fix(explore): invalid "No Filter" applied (#24876)
    
    (cherry picked from commit 371bffbfea3494f3a535e148fbe99b17a3a8a020)
---
 superset-frontend/src/explore/actions/saveModalActions.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/superset-frontend/src/explore/actions/saveModalActions.js b/superset-frontend/src/explore/actions/saveModalActions.js
index 1a9970ab75..32b34cbed4 100644
--- a/superset-frontend/src/explore/actions/saveModalActions.js
+++ b/superset-frontend/src/explore/actions/saveModalActions.js
@@ -21,6 +21,7 @@ import { SupersetClient, t } from '@superset-ui/core';
 import { addSuccessToast } from 'src/components/MessageToasts/actions';
 import { isEmpty } from 'lodash';
 import { buildV1ChartDataPayload } from '../exploreUtils';
+import { Operators } from '../constants';
 
 const ADHOC_FILTER_REGEX = /^adhoc_filters/;
 
@@ -81,7 +82,8 @@ export const getSlicePayload = (
   if (
     isEmpty(adhocFilters?.adhoc_filters) &&
     isEmpty(formDataFromSlice) &&
-    formDataWithNativeFilters?.adhoc_filters?.length > 0
+    formDataWithNativeFilters?.adhoc_filters?.[0]?.operator ===
+      Operators.TEMPORAL_RANGE
   ) {
     adhocFilters.adhoc_filters = [
       {


[superset] 06/16: fix: Python3.11 (str, Enum) issue (#24803)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 161e05445c2c02853be50c18d00fa22c3ceddeb4
Author: EugeneTorap <ev...@gmail.com>
AuthorDate: Mon Jul 31 19:04:09 2023 +0300

    fix: Python3.11 (str, Enum) issue (#24803)
---
 .pylintrc                                          |  1 +
 superset/common/chart_data.py                      |  6 ++---
 superset/common/db_query_status.py                 |  4 +--
 superset/connectors/base/models.py                 |  4 +--
 superset/constants.py                              |  8 +++---
 superset/errors.py                                 |  7 +++---
 superset/key_value/types.py                        |  6 ++---
 superset/models/core.py                            |  6 ++---
 superset/reports/models.py                         | 17 ++++++-------
 superset/sql_parse.py                              |  6 ++---
 superset/sqllab/limiting_factor.py                 |  4 +--
 superset/tasks/types.py                            |  5 ++--
 .../limiting_factor.py => utils/backports.py}      | 15 +++++------
 superset/utils/core.py                             | 29 +++++++++++-----------
 tests/unit_tests/utils/test_decorators.py          |  4 +--
 15 files changed, 63 insertions(+), 59 deletions(-)

diff --git a/.pylintrc b/.pylintrc
index b39335c56b..08e44d3fae 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -83,6 +83,7 @@ enable=
 # no Warning level messages displayed, use"--disable=all --enable=classes
 # --disable=W"
 disable=
+    no-member,  # re-enable once this no longer raises false positives. This will become redundant after the min required version is 3.11
     missing-docstring,
     duplicate-code,
     unspecified-encoding,
diff --git a/superset/common/chart_data.py b/superset/common/chart_data.py
index 65c0c43c11..e03f51eee2 100644
--- a/superset/common/chart_data.py
+++ b/superset/common/chart_data.py
@@ -14,10 +14,10 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from enum import Enum
+from superset.utils.backports import StrEnum
 
 
-class ChartDataResultFormat(str, Enum):
+class ChartDataResultFormat(StrEnum):
     """
     Chart data response format
     """
@@ -31,7 +31,7 @@ class ChartDataResultFormat(str, Enum):
         return {cls.CSV} | {cls.XLSX}
 
 
-class ChartDataResultType(str, Enum):
+class ChartDataResultType(StrEnum):
     """
     Chart data response type
     """
diff --git a/superset/common/db_query_status.py b/superset/common/db_query_status.py
index 82bb437f65..fa893a1717 100644
--- a/superset/common/db_query_status.py
+++ b/superset/common/db_query_status.py
@@ -14,10 +14,10 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from enum import Enum
+from superset.utils.backports import StrEnum
 
 
-class QueryStatus(str, Enum):
+class QueryStatus(StrEnum):
     """Enum-type class for query statuses"""
 
     STOPPED: str = "stopped"
diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py
index 56e5ef18ad..706c82635c 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -21,7 +21,6 @@ import json
 import logging
 from collections.abc import Hashable
 from datetime import datetime
-from enum import Enum
 from json.decoder import JSONDecodeError
 from typing import Any, TYPE_CHECKING
 
@@ -44,6 +43,7 @@ from superset.superset_typing import (
     ResultSetColumnType,
 )
 from superset.utils import core as utils
+from superset.utils.backports import StrEnum
 from superset.utils.core import GenericDataType, MediumText
 
 if TYPE_CHECKING:
@@ -75,7 +75,7 @@ COLUMN_FORM_DATA_PARAMS = [
 ]
 
 
-class DatasourceKind(str, Enum):
+class DatasourceKind(StrEnum):
     VIRTUAL = "virtual"
     PHYSICAL = "physical"
 
diff --git a/superset/constants.py b/superset/constants.py
index 48b08506ca..4f01674bd4 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -20,6 +20,8 @@
 # string to use when None values *need* to be converted to/from strings
 from enum import Enum
 
+from superset.utils.backports import StrEnum
+
 USER_AGENT = "Apache Superset"
 
 NULL_STRING = "<NULL>"
@@ -185,7 +187,7 @@ EXTRA_FORM_DATA_OVERRIDE_KEYS = (
 )
 
 
-class TimeGrain(str, Enum):
+class TimeGrain(StrEnum):
     SECOND = "PT1S"
     FIVE_SECONDS = "PT5S"
     THIRTY_SECONDS = "PT30S"
@@ -214,13 +216,13 @@ class PandasAxis(int, Enum):
     COLUMN = 1
 
 
-class PandasPostprocessingCompare(str, Enum):
+class PandasPostprocessingCompare(StrEnum):
     DIFF = "difference"
     PCT = "percentage"
     RAT = "ratio"
 
 
-class CacheRegion(str, Enum):
+class CacheRegion(StrEnum):
     DEFAULT = "default"
     DATA = "data"
     THUMBNAIL = "thumbnail"
diff --git a/superset/errors.py b/superset/errors.py
index 6f68f2466c..6661e98183 100644
--- a/superset/errors.py
+++ b/superset/errors.py
@@ -15,13 +15,14 @@
 # specific language governing permissions and limitations
 # under the License.
 from dataclasses import dataclass
-from enum import Enum
 from typing import Any, Optional
 
 from flask_babel import lazy_gettext as _
 
+from superset.utils.backports import StrEnum
 
-class SupersetErrorType(str, Enum):
+
+class SupersetErrorType(StrEnum):
     """
     Types of errors that can exist within Superset.
 
@@ -183,7 +184,7 @@ ERROR_TYPES_TO_ISSUE_CODES_MAPPING = {
 }
 
 
-class ErrorLevel(str, Enum):
+class ErrorLevel(StrEnum):
     """
     Levels of errors that can exist within Superset.
 
diff --git a/superset/key_value/types.py b/superset/key_value/types.py
index b2a47336c3..f5865de323 100644
--- a/superset/key_value/types.py
+++ b/superset/key_value/types.py
@@ -20,7 +20,6 @@ import json
 import pickle
 from abc import ABC, abstractmethod
 from dataclasses import dataclass
-from enum import Enum
 from typing import Any, TypedDict
 from uuid import UUID
 
@@ -30,6 +29,7 @@ from superset.key_value.exceptions import (
     KeyValueCodecDecodeException,
     KeyValueCodecEncodeException,
 )
+from superset.utils.backports import StrEnum
 
 
 @dataclass
@@ -44,14 +44,14 @@ class KeyValueFilter(TypedDict, total=False):
     uuid: UUID | None
 
 
-class KeyValueResource(str, Enum):
+class KeyValueResource(StrEnum):
     APP = "app"
     DASHBOARD_PERMALINK = "dashboard_permalink"
     EXPLORE_PERMALINK = "explore_permalink"
     METASTORE_CACHE = "superset_metastore_cache"
 
 
-class SharedKey(str, Enum):
+class SharedKey(StrEnum):
     DASHBOARD_PERMALINK_SALT = "dashboard_permalink_salt"
     EXPLORE_PERMALINK_SALT = "explore_permalink_salt"
 
diff --git a/superset/models/core.py b/superset/models/core.py
index a8fe8b5411..21f8eddba4 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -20,7 +20,6 @@
 from __future__ import annotations
 
 import builtins
-import enum
 import json
 import logging
 import textwrap
@@ -74,6 +73,7 @@ from superset.models.helpers import AuditMixinNullable, ImportExportMixin
 from superset.result_set import SupersetResultSet
 from superset.superset_typing import ResultSetColumnType
 from superset.utils import cache as cache_util, core as utils
+from superset.utils.backports import StrEnum
 from superset.utils.core import get_username
 
 config = app.config
@@ -116,7 +116,7 @@ class CssTemplate(Model, AuditMixinNullable):
     css = Column(Text, default="")
 
 
-class ConfigurationMethod(str, enum.Enum):
+class ConfigurationMethod(StrEnum):
     SQLALCHEMY_FORM = "sqlalchemy_form"
     DYNAMIC_FORM = "dynamic_form"
 
@@ -1007,7 +1007,7 @@ class Log(Model):  # pylint: disable=too-few-public-methods
     referrer = Column(String(1024))
 
 
-class FavStarClassName(str, enum.Enum):
+class FavStarClassName(StrEnum):
     CHART = "slice"
     DASHBOARD = "Dashboard"
 
diff --git a/superset/reports/models.py b/superset/reports/models.py
index a13ded6223..5c47f41456 100644
--- a/superset/reports/models.py
+++ b/superset/reports/models.py
@@ -15,8 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 """A collection of ORM sqlalchemy models for Superset"""
-import enum
-
 from cron_descriptor import get_description
 from flask_appbuilder import Model
 from flask_appbuilder.models.decorators import renders
@@ -41,28 +39,29 @@ from superset.models.dashboard import Dashboard
 from superset.models.helpers import AuditMixinNullable, ExtraJSONMixin
 from superset.models.slice import Slice
 from superset.reports.types import ReportScheduleExtra
+from superset.utils.backports import StrEnum
 
 metadata = Model.metadata  # pylint: disable=no-member
 
 
-class ReportScheduleType(str, enum.Enum):
+class ReportScheduleType(StrEnum):
     ALERT = "Alert"
     REPORT = "Report"
 
 
-class ReportScheduleValidatorType(str, enum.Enum):
+class ReportScheduleValidatorType(StrEnum):
     """Validator types for alerts"""
 
     NOT_NULL = "not null"
     OPERATOR = "operator"
 
 
-class ReportRecipientType(str, enum.Enum):
+class ReportRecipientType(StrEnum):
     EMAIL = "Email"
     SLACK = "Slack"
 
 
-class ReportState(str, enum.Enum):
+class ReportState(StrEnum):
     SUCCESS = "Success"
     WORKING = "Working"
     ERROR = "Error"
@@ -70,19 +69,19 @@ class ReportState(str, enum.Enum):
     GRACE = "On Grace"
 
 
-class ReportDataFormat(str, enum.Enum):
+class ReportDataFormat(StrEnum):
     VISUALIZATION = "PNG"
     DATA = "CSV"
     TEXT = "TEXT"
 
 
-class ReportCreationMethod(str, enum.Enum):
+class ReportCreationMethod(StrEnum):
     CHARTS = "charts"
     DASHBOARDS = "dashboards"
     ALERTS_REPORTS = "alerts_reports"
 
 
-class ReportSourceFormat(str, enum.Enum):
+class ReportSourceFormat(StrEnum):
     CHART = "chart"
     DASHBOARD = "dashboard"
 
diff --git a/superset/sql_parse.py b/superset/sql_parse.py
index 974d7eacd4..a6d4806707 100644
--- a/superset/sql_parse.py
+++ b/superset/sql_parse.py
@@ -18,7 +18,6 @@ import logging
 import re
 from collections.abc import Iterator
 from dataclasses import dataclass
-from enum import Enum
 from typing import Any, cast, Optional
 from urllib import parse
 
@@ -49,6 +48,7 @@ from sqlparse.tokens import (
 from sqlparse.utils import imt
 
 from superset.exceptions import QueryClauseValidationException
+from superset.utils.backports import StrEnum
 
 try:
     from sqloxide import parse_sql as sqloxide_parse
@@ -71,7 +71,7 @@ sqlparser_sql_regex.insert(25, (r"'(''|\\\\|\\|[^'])*'", sqlparse.tokens.String.
 lex.set_SQL_REGEX(sqlparser_sql_regex)
 
 
-class CtasMethod(str, Enum):
+class CtasMethod(StrEnum):
     TABLE = "TABLE"
     VIEW = "VIEW"
 
@@ -483,7 +483,7 @@ def sanitize_clause(clause: str) -> str:
     return clause
 
 
-class InsertRLSState(str, Enum):
+class InsertRLSState(StrEnum):
     """
     State machine that scans for WHERE and ON clauses referencing tables.
     """
diff --git a/superset/sqllab/limiting_factor.py b/superset/sqllab/limiting_factor.py
index 46cbc9bd81..638f9e347a 100644
--- a/superset/sqllab/limiting_factor.py
+++ b/superset/sqllab/limiting_factor.py
@@ -14,10 +14,10 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-import enum
+from superset.utils.backports import StrEnum
 
 
-class LimitingFactor(str, enum.Enum):
+class LimitingFactor(StrEnum):
     QUERY = "QUERY"
     DROPDOWN = "DROPDOWN"
     QUERY_AND_DROPDOWN = "QUERY_AND_DROPDOWN"
diff --git a/superset/tasks/types.py b/superset/tasks/types.py
index cc337a81ed..84a3e7b01f 100644
--- a/superset/tasks/types.py
+++ b/superset/tasks/types.py
@@ -14,11 +14,10 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+from superset.utils.backports import StrEnum
 
-from enum import Enum
 
-
-class ExecutorType(str, Enum):
+class ExecutorType(StrEnum):
     """
     Which user should scheduled tasks be executed as. Used as follows:
     For Alerts & Reports: the "model" refers to the AlertSchedule object
diff --git a/superset/sqllab/limiting_factor.py b/superset/utils/backports.py
similarity index 79%
copy from superset/sqllab/limiting_factor.py
copy to superset/utils/backports.py
index 46cbc9bd81..b48f76d235 100644
--- a/superset/sqllab/limiting_factor.py
+++ b/superset/utils/backports.py
@@ -14,12 +14,13 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-import enum
+import sys
+from enum import Enum
 
+if sys.version_info >= (3, 11):
+    # pylint: disable=unused-import
+    from enum import StrEnum  # nopycln: import
+else:
 
-class LimitingFactor(str, enum.Enum):
-    QUERY = "QUERY"
-    DROPDOWN = "DROPDOWN"
-    QUERY_AND_DROPDOWN = "QUERY_AND_DROPDOWN"
-    NOT_LIMITED = "NOT_LIMITED"
-    UNKNOWN = "UNKNOWN"
+    class StrEnum(str, Enum):
+        pass
diff --git a/superset/utils/core.py b/superset/utils/core.py
index ca3b9ae7b7..f40ccb63a2 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -98,6 +98,7 @@ from superset.superset_typing import (
     FormData,
     Metric,
 )
+from superset.utils.backports import StrEnum
 from superset.utils.database import get_example_database
 from superset.utils.date_parser import parse_human_timedelta
 from superset.utils.dates import datetime_to_epoch, EPOCH
@@ -133,12 +134,12 @@ class LenientEnum(Enum):
             return None
 
 
-class AdhocMetricExpressionType(str, Enum):
+class AdhocMetricExpressionType(StrEnum):
     SIMPLE = "SIMPLE"
     SQL = "SQL"
 
 
-class AnnotationType(str, Enum):
+class AnnotationType(StrEnum):
     FORMULA = "FORMULA"
     INTERVAL = "INTERVAL"
     EVENT = "EVENT"
@@ -160,7 +161,7 @@ class GenericDataType(IntEnum):
     # ROW = 7
 
 
-class DatasourceType(str, Enum):
+class DatasourceType(StrEnum):
     SLTABLE = "sl_table"
     TABLE = "table"
     DATASET = "dataset"
@@ -169,7 +170,7 @@ class DatasourceType(str, Enum):
     VIEW = "view"
 
 
-class LoggerLevel(str, Enum):
+class LoggerLevel(StrEnum):
     INFO = "info"
     WARNING = "warning"
     EXCEPTION = "exception"
@@ -208,19 +209,19 @@ class QueryObjectFilterClause(TypedDict, total=False):
     isExtra: bool | None
 
 
-class ExtraFiltersTimeColumnType(str, Enum):
+class ExtraFiltersTimeColumnType(StrEnum):
     TIME_COL = "__time_col"
     TIME_GRAIN = "__time_grain"
     TIME_ORIGIN = "__time_origin"
     TIME_RANGE = "__time_range"
 
 
-class ExtraFiltersReasonType(str, Enum):
+class ExtraFiltersReasonType(StrEnum):
     NO_TEMPORAL_COLUMN = "no_temporal_column"
     COL_NOT_IN_DATASOURCE = "not_in_datasource"
 
 
-class FilterOperator(str, Enum):
+class FilterOperator(StrEnum):
     """
     Operators used filter controls
     """
@@ -242,7 +243,7 @@ class FilterOperator(str, Enum):
     TEMPORAL_RANGE = "TEMPORAL_RANGE"
 
 
-class FilterStringOperators(str, Enum):
+class FilterStringOperators(StrEnum):
     EQUALS = ("EQUALS",)
     NOT_EQUALS = ("NOT_EQUALS",)
     LESS_THAN = ("LESS_THAN",)
@@ -260,7 +261,7 @@ class FilterStringOperators(str, Enum):
     IS_FALSE = ("IS_FALSE",)
 
 
-class PostProcessingBoxplotWhiskerType(str, Enum):
+class PostProcessingBoxplotWhiskerType(StrEnum):
     """
     Calculate cell contribution to row/column total
     """
@@ -270,7 +271,7 @@ class PostProcessingBoxplotWhiskerType(str, Enum):
     PERCENTILE = "percentile"
 
 
-class PostProcessingContributionOrientation(str, Enum):
+class PostProcessingContributionOrientation(StrEnum):
     """
     Calculate cell contribution to row/column total
     """
@@ -298,7 +299,7 @@ class QuerySource(Enum):
     SQL_LAB = 2
 
 
-class QueryStatus(str, Enum):
+class QueryStatus(StrEnum):
     """Enum-type class for query statuses"""
 
     STOPPED: str = "stopped"
@@ -311,14 +312,14 @@ class QueryStatus(str, Enum):
     TIMED_OUT: str = "timed_out"
 
 
-class DashboardStatus(str, Enum):
+class DashboardStatus(StrEnum):
     """Dashboard status used for frontend filters"""
 
     PUBLISHED = "published"
     DRAFT = "draft"
 
 
-class ReservedUrlParameters(str, Enum):
+class ReservedUrlParameters(StrEnum):
     """
     Reserved URL parameters that are used internally by Superset. These will not be
     passed to chart queries, as they control the behavior of the UI.
@@ -336,7 +337,7 @@ class ReservedUrlParameters(str, Enum):
         return standalone
 
 
-class RowLevelSecurityFilterType(str, Enum):
+class RowLevelSecurityFilterType(StrEnum):
     REGULAR = "Regular"
     BASE = "Base"
 
diff --git a/tests/unit_tests/utils/test_decorators.py b/tests/unit_tests/utils/test_decorators.py
index 3aafc7a91d..f600334414 100644
--- a/tests/unit_tests/utils/test_decorators.py
+++ b/tests/unit_tests/utils/test_decorators.py
@@ -17,7 +17,6 @@
 
 
 from contextlib import nullcontext
-from enum import Enum
 from inspect import isclass
 from typing import Any, Optional
 from unittest.mock import call, Mock, patch
@@ -26,9 +25,10 @@ import pytest
 
 from superset import app
 from superset.utils import decorators
+from superset.utils.backports import StrEnum
 
 
-class ResponseValues(str, Enum):
+class ResponseValues(StrEnum):
     FAIL = "fail"
     WARN = "warn"
     OK = "ok"


[superset] 07/16: fix(embedded): adding logic to check dataset used by filters (#24808)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit bbe4e016d8b0bdca247f46d84912f8e90454362a
Author: Vitor Avila <96...@users.noreply.github.com>
AuthorDate: Mon Jul 31 18:10:57 2023 -0300

    fix(embedded): adding logic to check dataset used by filters (#24808)
    
    (cherry picked from commit 7f9b0380e0e9f192402efda014ff39469881859b)
---
 superset/security/manager.py                       | 25 +++++++++++-
 .../security/guest_token_security_tests.py         | 46 ++++++++++++++++++++++
 2 files changed, 69 insertions(+), 2 deletions(-)

diff --git a/superset/security/manager.py b/superset/security/manager.py
index 28354ac18d..b4b6643976 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -16,6 +16,7 @@
 # under the License.
 # pylint: disable=too-many-lines
 """A set of constants and methods to manage permissions and security"""
+import json
 import logging
 import re
 import time
@@ -2062,8 +2063,28 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
             .filter(Dashboard.id.in_(dashboard_ids))
         )
 
-        exists = db.session.query(query.exists()).scalar()
-        return exists
+        if db.session.query(query.exists()).scalar():
+            return True
+
+        # check for datasets that are only used by filters
+        dashboards_json = (
+            db.session.query(Dashboard.json_metadata)
+            .filter(Dashboard.id.in_(dashboard_ids))
+            .all()
+        )
+        for json_ in dashboards_json:
+            try:
+                json_metadata = json.loads(json_.json_metadata)
+                for filter_ in json_metadata.get("native_filter_configuration", []):
+                    filter_dataset_ids = [
+                        target.get("datasetId") for target in filter_.get("targets", [])
+                    ]
+                    if datasource.id in filter_dataset_ids:
+                        return True
+            except ValueError:
+                pass
+
+        return False
 
     @staticmethod
     def _get_current_epoch_time() -> float:
diff --git a/tests/integration_tests/security/guest_token_security_tests.py b/tests/integration_tests/security/guest_token_security_tests.py
index e0517c9b28..e5bf589184 100644
--- a/tests/integration_tests/security/guest_token_security_tests.py
+++ b/tests/integration_tests/security/guest_token_security_tests.py
@@ -15,18 +15,21 @@
 # specific language governing permissions and limitations
 # under the License.
 """Unit tests for Superset"""
+import json
 from unittest import mock
 
 import pytest
 from flask import g
 
 from superset import db, security_manager
+from superset.connectors.sqla.models import SqlaTable
 from superset.daos.dashboard import EmbeddedDashboardDAO
 from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
 from superset.exceptions import SupersetSecurityException
 from superset.models.dashboard import Dashboard
 from superset.security.guest_token import GuestTokenResourceType
 from superset.sql_parse import Table
+from superset.utils.database import get_example_database
 from tests.integration_tests.base_tests import SupersetTestCase
 from tests.integration_tests.fixtures.birth_names_dashboard import (
     load_birth_names_dashboard_with_slices,
@@ -233,3 +236,46 @@ class TestGuestUserDashboardAccess(SupersetTestCase):
 
         db.session.delete(dash)
         db.session.commit()
+
+    def test_can_access_datasource_used_in_dashboard_filter(self):
+        """
+        Test that a user can access a datasource used only by a filter in a dashboard
+        they have access to.
+        """
+        # Create a test dataset
+        test_dataset = SqlaTable(
+            database_id=get_example_database().id,
+            schema="main",
+            table_name="test_table_embedded_filter",
+        )
+        db.session.add(test_dataset)
+        db.session.commit()
+
+        # Create an embedabble dashboard with a filter powered by the test dataset
+        test_dashboard = Dashboard()
+        test_dashboard.dashboard_title = "Test Embedded Dashboard"
+        test_dashboard.json_metadata = json.dumps(
+            {
+                "native_filter_configuration": [
+                    {"targets": [{"datasetId": test_dataset.id}]}
+                ]
+            }
+        )
+        test_dashboard.owners = []
+        test_dashboard.slices = []
+        test_dashboard.published = False
+        db.session.add(test_dashboard)
+        db.session.commit()
+        self.embedded = EmbeddedDashboardDAO.upsert(test_dashboard, [])
+
+        # grant access to the dashboad
+        g.user = self.authorized_guest
+        g.user.resources = [{"type": "dashboard", "id": str(self.embedded.uuid)}]
+        g.user.roles = [security_manager.get_public_role()]
+
+        # The user should have access to the datasource via the dashboard
+        security_manager.raise_for_access(datasource=test_dataset)
+
+        db.session.delete(test_dashboard)
+        db.session.delete(test_dataset)
+        db.session.commit()


[superset] 01/16: fix: pass schema on dataset creation (#24815)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b89d7387a289fb643636147f01d67226509246fc
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Wed Jul 26 18:30:05 2023 -0700

    fix: pass schema on dataset creation (#24815)
    
    (cherry picked from commit ba508a786c2a33486155ef03d3fb9fb44cb69ec9)
---
 .../SaveDatasetModal/SaveDatasetModal.test.tsx     | 42 +++++++++++++++++++++-
 .../SqlLab/components/SaveDatasetModal/index.tsx   |  1 +
 superset-frontend/src/SqlLab/fixtures.ts           |  1 +
 3 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
index f707998d1d..4cac5c6204 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
@@ -18,10 +18,17 @@
  */
 import React from 'react';
 import * as reactRedux from 'react-redux';
-import { render, screen, cleanup, waitFor } from 'spec/helpers/testing-library';
+import {
+  fireEvent,
+  render,
+  screen,
+  cleanup,
+  waitFor,
+} from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
 import fetchMock from 'fetch-mock';
 import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
+import { createDatasource } from 'src/SqlLab/actions/sqlLab';
 import { user, testQuery, mockdatasets } from 'src/SqlLab/fixtures';
 
 const mockedProps = {
@@ -46,6 +53,15 @@ beforeEach(() => {
   cleanup();
 });
 
+// Mock the createDatasource action
+const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
+jest.mock('src/SqlLab/actions/sqlLab', () => ({
+  createDatasource: jest.fn(),
+}));
+jest.mock('src/explore/exploreUtils/formData', () => ({
+  postFormData: jest.fn(),
+}));
+
 describe('SaveDatasetModal', () => {
   it('renders a "Save as new" field', () => {
     render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
@@ -175,4 +191,28 @@ describe('SaveDatasetModal', () => {
     expect(screen.getByRole('button', { name: /back/i })).toBeVisible();
     expect(screen.getByRole('button', { name: /overwrite/i })).toBeVisible();
   });
+
+  it('sends the schema when creating the dataset', async () => {
+    const dummyDispatch = jest.fn().mockResolvedValue({});
+    useDispatchMock.mockReturnValue(dummyDispatch);
+    useSelectorMock.mockReturnValue({ ...user });
+
+    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+
+    const inputFieldText = screen.getByDisplayValue(/unimportant/i);
+    fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
+
+    const saveConfirmationBtn = screen.getByRole('button', {
+      name: /save/i,
+    });
+    userEvent.click(saveConfirmationBtn);
+
+    expect(createDatasource).toHaveBeenCalledWith({
+      datasourceName: 'my dataset',
+      dbId: 1,
+      schema: 'main',
+      sql: 'SELECT *',
+      templateParams: undefined,
+    });
+  });
 });
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
index a42928608a..7f605967ad 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
@@ -296,6 +296,7 @@ export const SaveDatasetModal = ({
       createDatasource({
         sql: datasource.sql,
         dbId: datasource.dbId || datasource?.database?.id,
+        schema: datasource?.schema,
         templateParams,
         datasourceName: datasetName,
       }),
diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts
index 658c1432db..0afd1c4149 100644
--- a/superset-frontend/src/SqlLab/fixtures.ts
+++ b/superset-frontend/src/SqlLab/fixtures.ts
@@ -689,6 +689,7 @@ export const queryId = 'clientId2353';
 export const testQuery: ISaveableDatasource = {
   name: 'unimportant',
   dbId: 1,
+  schema: 'main',
   sql: 'SELECT *',
   columns: [
     {


[superset] 04/16: fix: Allow chart import to update the dataset an existing chart points to (#24821)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit a9b8c8e3ec24671a6df563820423a84c79220667
Author: Jack Fragassi <jf...@gmail.com>
AuthorDate: Fri Jul 28 16:08:03 2023 -0700

    fix: Allow chart import to update the dataset an existing chart points to (#24821)
    
    (cherry picked from commit 77889b29fb0e50473ca7656be4e5bf2f1dff5421)
---
 superset/charts/commands/importers/v1/utils.py |  4 +++-
 superset/models/helpers.py                     | 14 +++++++++-----
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/superset/charts/commands/importers/v1/utils.py b/superset/charts/commands/importers/v1/utils.py
index 589ae76a31..d3f90f7ff4 100644
--- a/superset/charts/commands/importers/v1/utils.py
+++ b/superset/charts/commands/importers/v1/utils.py
@@ -53,7 +53,9 @@ def import_chart(
     # migrate old viz types to new ones
     config = migrate_chart(config)
 
-    chart = Slice.import_from_dict(session, config, recursive=False)
+    chart = Slice.import_from_dict(
+        session, config, recursive=False, allow_reparenting=True
+    )
     if chart.id is None:
         session.flush()
 
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index 9e269652c7..8761e90933 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -182,7 +182,7 @@ class ImportExportMixin:
     __mapper__: Mapper
 
     @classmethod
-    def _unique_constrains(cls) -> list[set[str]]:
+    def _unique_constraints(cls) -> list[set[str]]:
         """Get all (single column and multi column) unique constraints"""
         unique = [
             {c.name for c in u.columns}
@@ -244,6 +244,7 @@ class ImportExportMixin:
         parent: Optional[Any] = None,
         recursive: bool = True,
         sync: Optional[list[str]] = None,
+        allow_reparenting: bool = False,
     ) -> Any:
         """Import obj from a dictionary"""
         if sync is None:
@@ -256,7 +257,7 @@ class ImportExportMixin:
             | {"uuid"}
         )
         new_children = {c: dict_rep[c] for c in cls.export_children if c in dict_rep}
-        unique_constrains = cls._unique_constrains()
+        unique_constraints = cls._unique_constraints()
 
         filters = []  # Using these filters to check if obj already exists
 
@@ -275,8 +276,11 @@ class ImportExportMixin:
             for k, v in parent_refs.items():
                 dict_rep[k] = getattr(parent, v)
 
-        # Add filter for parent obj
-        filters.extend([getattr(cls, k) == dict_rep.get(k) for k in parent_refs.keys()])
+        if not allow_reparenting:
+            # Add filter for parent obj
+            filters.extend(
+                [getattr(cls, k) == dict_rep.get(k) for k in parent_refs.keys()]
+            )
 
         # Add filter for unique constraints
         ucs = [
@@ -287,7 +291,7 @@ class ImportExportMixin:
                     if dict_rep.get(k) is not None
                 ]
             )
-            for cs in unique_constrains
+            for cs in unique_constraints
         ]
         filters.append(or_(*ucs))
 


[superset] 13/16: fix(annotation): Address regression from #24694 (#24874)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 21764f9ae37c72cf35bda7a4547c8ff539b51094
Author: John Bodley <45...@users.noreply.github.com>
AuthorDate: Thu Aug 3 05:33:03 2023 -0700

    fix(annotation): Address regression from #24694 (#24874)
    
    (cherry picked from commit f05638ba845596faef088efa3ee98686d26dad26)
---
 .../packages/superset-ui-core/src/query/types/AnnotationLayer.ts       | 3 +--
 .../packages/superset-ui-core/test/query/types/AnnotationLayer.test.ts | 1 -
 .../plugins/plugin-chart-echarts/test/utils/annotation.test.ts         | 1 -
 3 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/AnnotationLayer.ts b/superset-frontend/packages/superset-ui-core/src/query/types/AnnotationLayer.ts
index bac743cb25..d5b2981163 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/AnnotationLayer.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/AnnotationLayer.ts
@@ -159,7 +159,6 @@ export function isTableAnnotationLayer(
 }
 
 export type RecordAnnotationResult = {
-  columns: string[];
   records: DataRecord[];
 };
 
@@ -181,7 +180,7 @@ export function isTimeseriesAnnotationResult(
 export function isRecordAnnotationResult(
   result: any,
 ): result is RecordAnnotationResult {
-  return Array.isArray(result?.columns) && Array.isArray(result?.records);
+  return Array.isArray(result?.records);
 }
 
 export type AnnotationData = { [key: string]: AnnotationResult };
diff --git a/superset-frontend/packages/superset-ui-core/test/query/types/AnnotationLayer.test.ts b/superset-frontend/packages/superset-ui-core/test/query/types/AnnotationLayer.test.ts
index c586239fd7..f6a695eb91 100644
--- a/superset-frontend/packages/superset-ui-core/test/query/types/AnnotationLayer.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/query/types/AnnotationLayer.test.ts
@@ -93,7 +93,6 @@ describe('AnnotationLayer type guards', () => {
     },
   ];
   const recordAnnotationResult: RecordAnnotationResult = {
-    columns: ['col1', 'col2'],
     records: [
       { a: 1, b: 2 },
       { a: 2, b: 3 },
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/annotation.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/annotation.test.ts
index bef98483be..59afebd736 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/annotation.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/annotation.test.ts
@@ -130,7 +130,6 @@ describe('extractAnnotationLabels', () => {
     ];
     const results: AnnotationData = {
       'My Interval': {
-        columns: ['col'],
         records: [{ col: 1 }],
       },
       'My Line': [