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 2024/04/16 11:45:40 UTC

(superset) branch 4.0 updated (ab950f8729 -> f8369d3a5d)

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

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


 discard ab950f8729 fix: Dynamic filter does not show all values on blur/clear events (#28036)
 discard 61d6b9c429 fix: bump client side chart timeouts to use the SUPERSET_WEBSERVER_TIMEOUT (#28018)
 discard bf6cd566d1 fix: Select is accepting unknown pasted values when `allowNewOptions` is false (#28017)
 discard ffd8baab80 fix: Incorrect onChange value when an unloaded value is pasted into AsyncSelect (#27996)
 discard 8c113b2fab fix(time_offset): improved LIMIT-handling in advanced analytics (#27934)
 discard 67ccada887 fix(drillby): Enable DrillBy in charts w/o filters (dimensions) (#27941)
 discard 164235f370 fix(superset-frontend): remove unused `@superset-ui/plugin-chart-period-over-period-kpi` package (#27994)
     new cd2cbbdc7b fix(superset-frontend): remove unused `@superset-ui/plugin-chart-period-over-period-kpi` package (#27994)
     new 55c8d6c1e5 fix(drillby): Enable DrillBy in charts w/o filters (dimensions) (#27941)
     new fa21f44502 fix(time_offset): improved LIMIT-handling in advanced analytics (#27934)
     new 37461de8fb fix: Incorrect onChange value when an unloaded value is pasted into AsyncSelect (#27996)
     new b5a878a047 fix: Select is accepting unknown pasted values when `allowNewOptions` is false (#28017)
     new 5f09d95d1b fix: bump client side chart timeouts to use the SUPERSET_WEBSERVER_TIMEOUT (#28018)
     new f8369d3a5d fix: Dynamic filter does not show all values on blur/clear events (#28036)

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (ab950f8729)
            \
             N -- N -- N   refs/heads/4.0 (f8369d3a5d)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 7 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:
 superset-frontend/package-lock.json | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)


(superset) 06/07: fix: bump client side chart timeouts to use the SUPERSET_WEBSERVER_TIMEOUT (#28018)

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

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

commit 5f09d95d1b7d91c72500e8ebf46fe994192659b4
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Mon Apr 15 13:16:18 2024 -0700

    fix: bump client side chart timeouts to use the SUPERSET_WEBSERVER_TIMEOUT (#28018)
    
    (cherry picked from commit 99c414e4dad9ad608d41af04de0c6f7f53758960)
---
 superset-frontend/src/SqlLab/fixtures.ts           |   1 +
 .../src/components/Chart/chartAction.js            |  21 ++--
 .../src/components/Chart/chartActions.test.js      | 117 +++++++++++++++++----
 3 files changed, 112 insertions(+), 27 deletions(-)

diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts
index 845e2209b5..742145c58c 100644
--- a/superset-frontend/src/SqlLab/fixtures.ts
+++ b/superset-frontend/src/SqlLab/fixtures.ts
@@ -679,6 +679,7 @@ export const initialState = {
       DISPLAY_MAX_ROW: 100,
       SQLALCHEMY_DOCS_URL: 'test_SQLALCHEMY_DOCS_URL',
       SQLALCHEMY_DISPLAY_TEXT: 'test_SQLALCHEMY_DISPLAY_TEXT',
+      SUPERSET_WEBSERVER_TIMEOUT: '300',
     },
   },
 };
diff --git a/superset-frontend/src/components/Chart/chartAction.js b/superset-frontend/src/components/Chart/chartAction.js
index 9597a3cbb3..eb51021d0c 100644
--- a/superset-frontend/src/components/Chart/chartAction.js
+++ b/superset-frontend/src/components/Chart/chartAction.js
@@ -248,17 +248,20 @@ export async function getChartDataRequest({
 
 export function runAnnotationQuery({
   annotation,
-  timeout = 60,
+  timeout,
   formData = null,
   key,
   isDashboardRequest = false,
   force = false,
 }) {
   return function (dispatch, getState) {
-    const sliceKey = key || Object.keys(getState().charts)[0];
+    const { charts, common } = getState();
+    const sliceKey = key || Object.keys(charts)[0];
+    const queryTimeout = timeout || common.conf.SUPERSET_WEBSERVER_TIMEOUT;
+
     // make a copy of formData, not modifying original formData
     const fd = {
-      ...(formData || getState().charts[sliceKey].latestQueryFormData),
+      ...(formData || charts[sliceKey].latestQueryFormData),
     };
 
     if (!annotation.sourceType) {
@@ -309,7 +312,7 @@ export function runAnnotationQuery({
     return SupersetClient.post({
       url,
       signal,
-      timeout: timeout * 1000,
+      timeout: queryTimeout * 1000,
       headers: { 'Content-Type': 'application/json' },
       jsonPayload: buildV1ChartDataPayload({
         formData: fd,
@@ -396,18 +399,20 @@ export function handleChartDataResponse(response, json, useLegacyApi) {
 export function exploreJSON(
   formData,
   force = false,
-  timeout = 60,
+  timeout,
   key,
   dashboardId,
   ownState,
 ) {
-  return async dispatch => {
+  return async (dispatch, getState) => {
     const logStart = Logger.getTimestamp();
     const controller = new AbortController();
+    const queryTimeout =
+      timeout || getState().common.conf.SUPERSET_WEBSERVER_TIMEOUT;
 
     const requestParams = {
       signal: controller.signal,
-      timeout: timeout * 1000,
+      timeout: queryTimeout * 1000,
     };
     if (dashboardId) requestParams.dashboard_id = dashboardId;
 
@@ -519,7 +524,7 @@ export const POST_CHART_FORM_DATA = 'POST_CHART_FORM_DATA';
 export function postChartFormData(
   formData,
   force = false,
-  timeout = 60,
+  timeout,
   key,
   dashboardId,
   ownState,
diff --git a/superset-frontend/src/components/Chart/chartActions.test.js b/superset-frontend/src/components/Chart/chartActions.test.js
index c2a58e6094..129c17dbe0 100644
--- a/superset-frontend/src/components/Chart/chartActions.test.js
+++ b/superset-frontend/src/components/Chart/chartActions.test.js
@@ -28,6 +28,27 @@ import * as actions from 'src/components/Chart/chartAction';
 import * as asyncEvent from 'src/middleware/asyncEvent';
 import { handleChartDataResponse } from 'src/components/Chart/chartAction';
 
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { initialState } from 'src/SqlLab/fixtures';
+
+const middlewares = [thunk];
+const mockStore = configureMockStore(middlewares);
+
+const mockGetState = () => ({
+  charts: {
+    chartKey: {
+      latestQueryFormData: {
+        time_grain_sqla: 'P1D',
+        granularity_sqla: 'Date',
+      },
+    },
+  },
+  common: {
+    conf: {},
+  },
+});
+
 describe('chart actions', () => {
   const MOCK_URL = '/mockURL';
   let dispatch;
@@ -94,7 +115,7 @@ describe('chart actions', () => {
 
     it('should query with the built query', async () => {
       const actionThunk = actions.postChartFormData({}, null);
-      await actionThunk(dispatch);
+      await actionThunk(dispatch, mockGetState);
 
       expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
       expect(fetchMock.calls(MOCK_URL)[0][1].body).toBe(
@@ -165,7 +186,7 @@ describe('chart actions', () => {
     it('should dispatch CHART_UPDATE_STARTED action before the query', () => {
       const actionThunk = actions.postChartFormData({});
 
-      return actionThunk(dispatch).then(() => {
+      return actionThunk(dispatch, mockGetState).then(() => {
         // chart update, trigger query, update form data, success
         expect(dispatch.callCount).toBe(5);
         expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
@@ -175,7 +196,7 @@ describe('chart actions', () => {
 
     it('should dispatch TRIGGER_QUERY action with the query', () => {
       const actionThunk = actions.postChartFormData({});
-      return actionThunk(dispatch).then(() => {
+      return actionThunk(dispatch, mockGetState).then(() => {
         // chart update, trigger query, update form data, success
         expect(dispatch.callCount).toBe(5);
         expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
@@ -185,7 +206,7 @@ describe('chart actions', () => {
 
     it('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
       const actionThunk = actions.postChartFormData({});
-      return actionThunk(dispatch).then(() => {
+      return actionThunk(dispatch, mockGetState).then(() => {
         // chart update, trigger query, update form data, success
         expect(dispatch.callCount).toBe(5);
         expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
@@ -195,7 +216,7 @@ describe('chart actions', () => {
 
     it('should dispatch logEvent async action', () => {
       const actionThunk = actions.postChartFormData({});
-      return actionThunk(dispatch).then(() => {
+      return actionThunk(dispatch, mockGetState).then(() => {
         // chart update, trigger query, update form data, success
         expect(dispatch.callCount).toBe(5);
         expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
@@ -209,7 +230,7 @@ describe('chart actions', () => {
 
     it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
       const actionThunk = actions.postChartFormData({});
-      return actionThunk(dispatch).then(() => {
+      return actionThunk(dispatch, mockGetState).then(() => {
         // chart update, trigger query, update form data, success
         expect(dispatch.callCount).toBe(5);
         expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
@@ -226,7 +247,7 @@ describe('chart actions', () => {
       const timeoutInSec = 1 / 1000;
       const actionThunk = actions.postChartFormData({}, false, timeoutInSec);
 
-      return actionThunk(dispatch).then(() => {
+      return actionThunk(dispatch, mockGetState).then(() => {
         // chart update, trigger query, update form data, fail
         expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
         expect(dispatch.callCount).toBe(5);
@@ -245,7 +266,7 @@ describe('chart actions', () => {
       const timeoutInSec = 100; // Set to a time that is longer than the time this will take to fail
       const actionThunk = actions.postChartFormData({}, false, timeoutInSec);
 
-      return actionThunk(dispatch).then(() => {
+      return actionThunk(dispatch, mockGetState).then(() => {
         // chart update, trigger query, update form data, fail
         expect(dispatch.callCount).toBe(5);
         const updateFailedAction = dispatch.args[4][0];
@@ -278,17 +299,6 @@ describe('chart actions', () => {
 
   describe('runAnnotationQuery', () => {
     const mockDispatch = jest.fn();
-    const mockGetState = () => ({
-      charts: {
-        chartKey: {
-          latestQueryFormData: {
-            time_grain_sqla: 'P1D',
-            granularity_sqla: 'Date',
-          },
-        },
-      },
-    });
-
     beforeEach(() => {
       jest.clearAllMocks();
     });
@@ -342,3 +352,72 @@ describe('chart actions', () => {
     });
   });
 });
+
+describe('chart actions timeout', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should use the timeout from arguments when given', () => {
+    const postSpy = jest.spyOn(SupersetClient, 'post');
+    postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
+    const timeout = 10; // Set the timeout value here
+    const formData = { datasource: 'table__1' }; // Set the formData here
+    const key = 'chartKey'; // Set the chart key here
+
+    const store = mockStore(initialState);
+    store.dispatch(
+      actions.runAnnotationQuery({
+        annotation: {
+          value: 'annotationValue',
+          sourceType: 'Event',
+          overrides: {},
+        },
+        timeout,
+        formData,
+        key,
+      }),
+    );
+
+    const expectedPayload = {
+      url: expect.any(String),
+      signal: expect.any(AbortSignal),
+      timeout: timeout * 1000,
+      headers: { 'Content-Type': 'application/json' },
+      jsonPayload: expect.any(Object),
+    };
+
+    expect(postSpy).toHaveBeenCalledWith(expectedPayload);
+  });
+
+  it('should use the timeout from common.conf when not passed as an argument', () => {
+    const postSpy = jest.spyOn(SupersetClient, 'post');
+    postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
+    const formData = { datasource: 'table__1' }; // Set the formData here
+    const key = 'chartKey'; // Set the chart key here
+
+    const store = mockStore(initialState);
+    store.dispatch(
+      actions.runAnnotationQuery({
+        annotation: {
+          value: 'annotationValue',
+          sourceType: 'Event',
+          overrides: {},
+        },
+        undefined,
+        formData,
+        key,
+      }),
+    );
+
+    const expectedPayload = {
+      url: expect.any(String),
+      signal: expect.any(AbortSignal),
+      timeout: initialState.common.conf.SUPERSET_WEBSERVER_TIMEOUT * 1000,
+      headers: { 'Content-Type': 'application/json' },
+      jsonPayload: expect.any(Object),
+    };
+
+    expect(postSpy).toHaveBeenCalledWith(expectedPayload);
+  });
+});


(superset) 07/07: fix: Dynamic filter does not show all values on blur/clear events (#28036)

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

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

commit f8369d3a5d7471a3eb191a9991c3e0d111fe8e68
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Tue Apr 16 08:11:56 2024 -0300

    fix: Dynamic filter does not show all values on blur/clear events (#28036)
    
    (cherry picked from commit 6e01a6827659881263648b621d7222ef84f0a74d)
---
 .../src/filters/components/Select/SelectFilterPlugin.tsx            | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
index 5b111b7dec..5bdd91bb64 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
@@ -189,7 +189,8 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
 
   const handleBlur = useCallback(() => {
     unsetFocusedFilter();
-  }, [unsetFocusedFilter]);
+    onSearch('');
+  }, [onSearch, unsetFocusedFilter]);
 
   const handleChange = useCallback(
     (value?: SelectValue | number | string) => {
@@ -293,7 +294,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
       >
         <Select
           allowClear
-          allowNewOptions
+          allowNewOptions={!searchAllOptions}
           allowSelectAll={!searchAllOptions}
           // @ts-ignore
           value={filterState.value || []}
@@ -307,6 +308,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
           showSearch={showSearch}
           mode={multiSelect ? 'multiple' : 'single'}
           placeholder={placeholderText}
+          onClear={() => onSearch('')}
           onSearch={onSearch}
           onBlur={handleBlur}
           onFocus={setFocusedFilter}


(superset) 05/07: fix: Select is accepting unknown pasted values when `allowNewOptions` is false (#28017)

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

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

commit b5a878a047a1cebda5f3490a1fb61ed2baae924f
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Mon Apr 15 09:33:17 2024 -0300

    fix: Select is accepting unknown pasted values when `allowNewOptions` is false (#28017)
    
    (cherry picked from commit caad29b5b36bdb817897b78c53c412f07916907f)
---
 .../src/components/Select/AsyncSelect.test.tsx     | 28 +++++++++++++++++++++-
 .../src/components/Select/AsyncSelect.tsx          | 16 +++++++++----
 .../src/components/Select/Select.test.tsx          | 14 +++++++++++
 superset-frontend/src/components/Select/Select.tsx | 14 ++++++++---
 4 files changed, 63 insertions(+), 9 deletions(-)

diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
index b862602638..652b1f0ea2 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
@@ -928,7 +928,14 @@ test('pasting an existing option does not duplicate it in multiple mode', async
     ],
     totalCount: 3,
   }));
-  render(<AsyncSelect {...defaultProps} options={options} mode="multiple" />);
+  render(
+    <AsyncSelect
+      {...defaultProps}
+      options={options}
+      mode="multiple"
+      allowNewOptions
+    />,
+  );
   await open();
   const input = getElementByClassName('.ant-select-selection-search-input');
   const paste = createEvent.paste(input, {
@@ -943,6 +950,25 @@ test('pasting an existing option does not duplicate it in multiple mode', async
   );
 });
 
+test('pasting an non-existent option should not add it if allowNewOptions is false', async () => {
+  render(
+    <AsyncSelect
+      {...defaultProps}
+      allowNewOptions={false}
+      options={async () => ({ data: [], totalCount: 0 })}
+    />,
+  );
+  await open();
+  const input = getElementByClassName('.ant-select-selection-search-input');
+  const paste = createEvent.paste(input, {
+    clipboardData: {
+      getData: () => 'John',
+    },
+  });
+  await waitFor(() => fireEvent(input, paste));
+  expect(await findAllSelectOptions()).toHaveLength(0);
+});
+
 test('onChange is called with the value property when pasting an option that was not loaded yet', async () => {
   const onChange = jest.fn();
   render(<AsyncSelect {...defaultProps} onChange={onChange} />);
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 16cd1b26b8..5468528893 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -543,6 +543,9 @@ const AsyncSelect = forwardRef(
               data.find(item => item.label === text),
           );
         }
+        if (!option && !allowNewOptions) {
+          return undefined;
+        }
         const value: AntdLabeledValue = {
           label: text,
           value: text,
@@ -553,19 +556,22 @@ const AsyncSelect = forwardRef(
         }
         return value;
       },
-      [allValuesLoaded, fullSelectOptions, options, pageSize],
+      [allValuesLoaded, allowNewOptions, fullSelectOptions, options, pageSize],
     );
 
     const onPaste = async (e: ClipboardEvent<HTMLInputElement>) => {
       const pastedText = e.clipboardData.getData('text');
       if (isSingleMode) {
-        setSelectValue(await getPastedTextValue(pastedText));
+        const value = await getPastedTextValue(pastedText);
+        if (value) {
+          setSelectValue(value);
+        }
       } else {
         const token = tokenSeparators.find(token => pastedText.includes(token));
         const array = token ? uniq(pastedText.split(token)) : [pastedText];
-        const values = await Promise.all(
-          array.map(item => getPastedTextValue(item)),
-        );
+        const values = (
+          await Promise.all(array.map(item => getPastedTextValue(item)))
+        ).filter(item => item !== undefined) as AntdLabeledValue[];
         setSelectValue(previous => [
           ...((previous || []) as AntdLabeledValue[]),
           ...values.filter(value => !hasOption(value.value, previous)),
diff --git a/superset-frontend/src/components/Select/Select.test.tsx b/superset-frontend/src/components/Select/Select.test.tsx
index 1daff06d4d..bbe09a27f4 100644
--- a/superset-frontend/src/components/Select/Select.test.tsx
+++ b/superset-frontend/src/components/Select/Select.test.tsx
@@ -1039,6 +1039,7 @@ test('pasting an existing option does not duplicate it in multiple mode', async
       options={options}
       mode="multiple"
       allowSelectAll={false}
+      allowNewOptions
     />,
   );
   await open();
@@ -1053,6 +1054,19 @@ test('pasting an existing option does not duplicate it in multiple mode', async
   expect(await findAllSelectOptions()).toHaveLength(4);
 });
 
+test('pasting an non-existent option should not add it if allowNewOptions is false', async () => {
+  render(<Select {...defaultProps} options={[]} allowNewOptions={false} />);
+  await open();
+  const input = getElementByClassName('.ant-select-selection-search-input');
+  const paste = createEvent.paste(input, {
+    clipboardData: {
+      getData: () => 'John',
+    },
+  });
+  fireEvent(input, paste);
+  expect(await findAllSelectOptions()).toHaveLength(0);
+});
+
 test('does not fire onChange if the same value is selected in single mode', async () => {
   const onChange = jest.fn();
   render(<Select {...defaultProps} onChange={onChange} />);
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 3db455cbe2..d92b86318d 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -543,6 +543,9 @@ const Select = forwardRef(
     const getPastedTextValue = useCallback(
       (text: string) => {
         const option = getOption(text, fullSelectOptions, true);
+        if (!option && !allowNewOptions) {
+          return undefined;
+        }
         if (labelInValue) {
           const value: AntdLabeledValue = {
             label: text,
@@ -556,17 +559,22 @@ const Select = forwardRef(
         }
         return option ? (isObject(option) ? option.value! : option) : text;
       },
-      [fullSelectOptions, labelInValue],
+      [allowNewOptions, fullSelectOptions, labelInValue],
     );
 
     const onPaste = (e: ClipboardEvent<HTMLInputElement>) => {
       const pastedText = e.clipboardData.getData('text');
       if (isSingleMode) {
-        setSelectValue(getPastedTextValue(pastedText));
+        const value = getPastedTextValue(pastedText);
+        if (value) {
+          setSelectValue(value);
+        }
       } else {
         const token = tokenSeparators.find(token => pastedText.includes(token));
         const array = token ? uniq(pastedText.split(token)) : [pastedText];
-        const values = array.map(item => getPastedTextValue(item));
+        const values = array
+          .map(item => getPastedTextValue(item))
+          .filter(item => item !== undefined);
         if (labelInValue) {
           setSelectValue(previous => [
             ...((previous || []) as AntdLabeledValue[]),


(superset) 01/07: fix(superset-frontend): remove unused `@superset-ui/plugin-chart-period-over-period-kpi` package (#27994)

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

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

commit cd2cbbdc7b85a63d541c123870d101132f6fbfb4
Author: Artem Gusev <co...@gmail.com>
AuthorDate: Thu Apr 11 21:27:22 2024 +0300

    fix(superset-frontend): remove unused `@superset-ui/plugin-chart-period-over-period-kpi` package (#27994)
---
 superset-frontend/package-lock.json | 5 ++---
 superset-frontend/package.json      | 1 -
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 9148048f47..4949789cbd 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "superset",
-  "version": "0.0.0-dev",
+  "version": "4.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "superset",
-      "version": "0.0.0-dev",
+      "version": "4.0.0",
       "license": "Apache-2.0",
       "workspaces": [
         "packages/*",
@@ -43,7 +43,6 @@
         "@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3",
         "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
         "@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
-        "@superset-ui/plugin-chart-period-over-period-kpi": "file:./plugins/plugin-chart-period-over-period-kpi",
         "@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
         "@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
         "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index a028089f74..9e2c9fc1c3 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -109,7 +109,6 @@
     "@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3",
     "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
     "@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
-    "@superset-ui/plugin-chart-period-over-period-kpi": "file:./plugins/plugin-chart-period-over-period-kpi",
     "@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
     "@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
     "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",


(superset) 02/07: fix(drillby): Enable DrillBy in charts w/o filters (dimensions) (#27941)

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

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

commit 55c8d6c1e5b94ce3a298c8d9b87738a979acda2d
Author: sowo <wo...@gmail.com>
AuthorDate: Thu Apr 11 20:47:02 2024 +0200

    fix(drillby): Enable DrillBy in charts w/o filters (dimensions) (#27941)
    
    (cherry picked from commit 35c8b7a162e1d3f5efa74e86ad71be6af584367b)
---
 .../Chart/DrillBy/DrillByMenuItems.test.tsx        |  4 +--
 .../components/Chart/DrillBy/DrillByMenuItems.tsx  |  4 +--
 .../Chart/DrillBy/useDrillByBreadcrumbs.tsx        | 42 +++++++++++++---------
 3 files changed, 29 insertions(+), 21 deletions(-)

diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
index 41cc6b3c08..c98ca48472 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
@@ -131,9 +131,9 @@ test('render disabled menu item for unsupported chart', async () => {
   );
 });
 
-test('render disabled menu item for supported chart, no filters', async () => {
+test('render enabled menu item for supported chart, no filters', async () => {
   renderMenu({ drillByConfig: { filters: [], groupbyFieldName: 'groupby' } });
-  await expectDrillByDisabled('Drill by is not available for this data point');
+  await expectDrillByEnabled();
 });
 
 test('render disabled menu item for supported chart, no columns', async () => {
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
index 2fe711474e..26ae3e2f32 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
@@ -107,9 +107,7 @@ export const DrillByMenuItems = ({
     setSearchInput('');
   }, [columns.length]);
 
-  const hasDrillBy =
-    ensureIsArray(drillByConfig?.filters).length &&
-    drillByConfig?.groupbyFieldName;
+  const hasDrillBy = drillByConfig?.groupbyFieldName;
 
   const handlesDimensionContextMenu = useMemo(
     () =>
diff --git a/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx b/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx
index fc7c0b2bf5..36f86371f8 100644
--- a/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx
@@ -34,8 +34,11 @@ export interface DrillByBreadcrumb {
   filters?: BinaryQueryObjectFilterClause[];
 }
 
-const BreadcrumbItem = styled(AntdBreadcrumb.Item)<{ isClickable: boolean }>`
-  ${({ theme, isClickable }) => css`
+const BreadcrumbItem = styled(AntdBreadcrumb.Item)<{
+  isClickable: boolean;
+  isHidden: boolean;
+}>`
+  ${({ theme, isClickable, isHidden }) => css`
     cursor: ${isClickable ? 'pointer' : 'auto'};
     color: ${theme.colors.grayscale.light1};
     transition: color ease-in ${theme.transitionTiming}s;
@@ -45,6 +48,7 @@ const BreadcrumbItem = styled(AntdBreadcrumb.Item)<{ isClickable: boolean }>`
     &:hover {
       color: ${isClickable ? theme.colors.grayscale.dark1 : 'inherit'};
     }
+    visibility: ${isHidden ? 'hidden' : 'visible'};
   `}
 `;
 
@@ -58,6 +62,9 @@ export const useDrillByBreadcrumbs = (
   useMemo(() => {
     // the last breadcrumb is not clickable
     const isClickable = (index: number) => index < breadcrumbsData.length - 1;
+    const isHidden = (breadcumb: DrillByBreadcrumb) =>
+      ensureIsArray(breadcumb.groupby).length === 0 &&
+      ensureIsArray(breadcumb.filters).length === 0;
     const getBreadcrumbText = (breadcrumb: DrillByBreadcrumb) =>
       `${ensureIsArray(breadcrumb.groupby)
         .map(column => column.verbose_name || column.column_name)
@@ -74,20 +81,23 @@ export const useDrillByBreadcrumbs = (
           margin: ${theme.gridUnit * 2}px 0 ${theme.gridUnit * 4}px;
         `}
       >
-        {breadcrumbsData.map((breadcrumb, index) => (
-          <BreadcrumbItem
-            key={index}
-            isClickable={isClickable(index)}
-            onClick={
-              isClickable(index)
-                ? () => onBreadcrumbClick(breadcrumb, index)
-                : noOp
-            }
-            data-test="drill-by-breadcrumb-item"
-          >
-            {getBreadcrumbText(breadcrumb)}
-          </BreadcrumbItem>
-        ))}
+        {breadcrumbsData
+          .map((breadcrumb, index) => (
+            <BreadcrumbItem
+              key={index}
+              isClickable={isClickable(index)}
+              isHidden={isHidden(breadcrumb)}
+              onClick={
+                isClickable(index)
+                  ? () => onBreadcrumbClick(breadcrumb, index)
+                  : noOp
+              }
+              data-test="drill-by-breadcrumb-item"
+            >
+              {getBreadcrumbText(breadcrumb)}
+            </BreadcrumbItem>
+          ))
+          .filter(item => item.props.isHidden === false)}
       </AntdBreadcrumb>
     );
   }, [breadcrumbsData, onBreadcrumbClick]);


(superset) 03/07: fix(time_offset): improved LIMIT-handling in advanced analytics (#27934)

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

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

commit fa21f445021979ca02d7099644585f87db0fc2bc
Author: Antonio Rivero <38...@users.noreply.github.com>
AuthorDate: Fri Apr 12 00:54:21 2024 +0200

    fix(time_offset): improved LIMIT-handling in advanced analytics (#27934)
    
    (cherry picked from commit 6844735a4513fb747780b346441f8da5107d0fe5)
---
 superset/common/query_context_processor.py     |  7 +++
 tests/integration_tests/query_context_tests.py | 67 +++++++++++++++++++++++++-
 2 files changed, 73 insertions(+), 1 deletion(-)

diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py
index f003f8ed30..55c80386a3 100644
--- a/superset/common/query_context_processor.py
+++ b/superset/common/query_context_processor.py
@@ -455,6 +455,13 @@ class QueryContextProcessor:
                 for metric in metric_names
             }
 
+            # When the original query has limit or offset we wont apply those
+            # to the subquery so we prevent data inconsistency due to missing records
+            # in the dataframes when performing the join
+            if query_object.row_limit or query_object.row_offset:
+                query_object_clone_dct["row_limit"] = config["ROW_LIMIT"]
+                query_object_clone_dct["row_offset"] = 0
+
             if isinstance(self._qc_datasource, Query):
                 result = self._qc_datasource.exc_query(query_object_clone_dct)
             else:
diff --git a/tests/integration_tests/query_context_tests.py b/tests/integration_tests/query_context_tests.py
index 0d6d69e4ce..3679009c85 100644
--- a/tests/integration_tests/query_context_tests.py
+++ b/tests/integration_tests/query_context_tests.py
@@ -17,13 +17,14 @@
 import re
 import time
 from typing import Any
+from unittest.mock import Mock, patch
 
 import numpy as np
 import pandas as pd
 import pytest
 from pandas import DateOffset
 
-from superset import db
+from superset import app, db
 from superset.charts.schemas import ChartDataQueryContextSchema
 from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
 from superset.common.query_context import QueryContext
@@ -689,6 +690,70 @@ class TestQueryContext(SupersetTestCase):
                     == df_3_years_later.loc[index]["sum__num"]
                 )
 
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    @patch("superset.common.query_context.QueryContext.get_query_result")
+    def test_time_offsets_in_query_object_no_limit(self, query_result_mock):
+        """
+        Ensure that time_offsets can generate the correct queries and
+        it doesnt use the row_limit nor row_offset from the original
+        query object
+        """
+        payload = get_query_context("birth_names")
+        payload["queries"][0]["columns"] = [
+            {
+                "timeGrain": "P1D",
+                "columnType": "BASE_AXIS",
+                "sqlExpression": "ds",
+                "label": "ds",
+                "expressionType": "SQL",
+            }
+        ]
+        payload["queries"][0]["metrics"] = ["sum__num"]
+        payload["queries"][0]["groupby"] = ["name"]
+        payload["queries"][0]["is_timeseries"] = True
+        payload["queries"][0]["row_limit"] = 100
+        payload["queries"][0]["row_offset"] = 10
+        payload["queries"][0]["time_range"] = "1990 : 1991"
+
+        initial_data = {
+            "__timestamp": ["1990-01-01", "1990-01-01"],
+            "name": ["zban", "ahwb"],
+            "sum__num": [43571, 27225],
+        }
+        initial_df = pd.DataFrame(initial_data)
+
+        mock_query_result = Mock()
+        mock_query_result.df = initial_df
+        side_effects = [mock_query_result]
+        query_result_mock.side_effect = side_effects
+        # Proceed with the test as before
+        query_context = ChartDataQueryContextSchema().load(payload)
+        query_object = query_context.queries[0]
+        # First call to get_query_result, should return initial_df
+        query_result = query_context.get_query_result(query_object)
+        df = query_result.df
+        # Setup the payload for time offsets
+        payload["queries"][0]["time_offsets"] = ["1 year ago", "1 year later"]
+        query_context = ChartDataQueryContextSchema().load(payload)
+        query_object = query_context.queries[0]
+        time_offsets_obj = query_context.processing_time_offsets(df, query_object)
+        sqls = time_offsets_obj["queries"]
+        row_limit_value = app.config["ROW_LIMIT"]
+        row_limit_pattern_with_config_value = r"LIMIT " + re.escape(
+            str(row_limit_value)
+        )
+        self.assertEqual(len(sqls), 2)
+        # 1 year ago
+        assert re.search(r"1989-01-01.+1990-01-01", sqls[0], re.S)
+        assert not re.search(r"LIMIT 100", sqls[0], re.S)
+        assert not re.search(r"OFFSET 10", sqls[0], re.S)
+        assert re.search(row_limit_pattern_with_config_value, sqls[0], re.S)
+        # 1 year later
+        assert re.search(r"1991-01-01.+1992-01-01", sqls[1], re.S)
+        assert not re.search(r"LIMIT 100", sqls[1], re.S)
+        assert not re.search(r"OFFSET 10", sqls[1], re.S)
+        assert re.search(row_limit_pattern_with_config_value, sqls[1], re.S)
+
 
 def test_get_label_map(app_context, virtual_dataset_comma_in_column_value):
     qc = QueryContextFactory().create(


(superset) 04/07: fix: Incorrect onChange value when an unloaded value is pasted into AsyncSelect (#27996)

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

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

commit 37461de8fb9e8e771b0223e23fccedec52339d11
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Fri Apr 12 11:08:05 2024 -0300

    fix: Incorrect onChange value when an unloaded value is pasted into AsyncSelect (#27996)
    
    (cherry picked from commit 06077d42a8b49dfa8789b2a4b0abcfb5b4d96c49)
---
 .../src/components/Select/AsyncSelect.test.tsx     | 42 +++++++++++++++++-----
 .../src/components/Select/AsyncSelect.tsx          | 23 ++++++++----
 2 files changed, 49 insertions(+), 16 deletions(-)

diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
index 8e8e151b66..b862602638 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
@@ -384,12 +384,14 @@ test('removes duplicated values', async () => {
     },
   });
   fireEvent(input, paste);
-  const values = await findAllSelectValues();
-  expect(values.length).toBe(4);
-  expect(values[0]).toHaveTextContent('a');
-  expect(values[1]).toHaveTextContent('b');
-  expect(values[2]).toHaveTextContent('c');
-  expect(values[3]).toHaveTextContent('d');
+  await waitFor(async () => {
+    const values = await findAllSelectValues();
+    expect(values.length).toBe(4);
+    expect(values[0]).toHaveTextContent('a');
+    expect(values[1]).toHaveTextContent('b');
+    expect(values[2]).toHaveTextContent('c');
+    expect(values[3]).toHaveTextContent('d');
+  });
 });
 
 test('renders a custom label', async () => {
@@ -879,7 +881,7 @@ test('fires onChange when pasting a selection', async () => {
     },
   });
   fireEvent(input, paste);
-  expect(onChange).toHaveBeenCalledTimes(1);
+  await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1));
 });
 
 test('does not duplicate options when using numeric values', async () => {
@@ -935,8 +937,30 @@ test('pasting an existing option does not duplicate it in multiple mode', async
     },
   });
   fireEvent(input, paste);
-  // Only Peter should be added
-  expect(await findAllSelectOptions()).toHaveLength(4);
+  await waitFor(async () =>
+    // Only Peter should be added
+    expect(await findAllSelectOptions()).toHaveLength(4),
+  );
+});
+
+test('onChange is called with the value property when pasting an option that was not loaded yet', async () => {
+  const onChange = jest.fn();
+  render(<AsyncSelect {...defaultProps} onChange={onChange} />);
+  await open();
+  const input = getElementByClassName('.ant-select-selection-search-input');
+  const lastOption = OPTIONS[OPTIONS.length - 1];
+  const paste = createEvent.paste(input, {
+    clipboardData: {
+      getData: () => lastOption.label,
+    },
+  });
+  fireEvent(input, paste);
+  await waitFor(() =>
+    expect(onChange).toHaveBeenCalledWith(
+      expect.objectContaining({ value: lastOption.value }),
+      expect.anything(),
+    ),
+  );
 });
 
 test('does not fire onChange if the same value is selected in single mode', async () => {
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 3fb8bceaf4..16cd1b26b8 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -534,8 +534,15 @@ const AsyncSelect = forwardRef(
     );
 
     const getPastedTextValue = useCallback(
-      (text: string) => {
-        const option = getOption(text, fullSelectOptions, true);
+      async (text: string) => {
+        let option = getOption(text, fullSelectOptions, true);
+        if (!option && !allValuesLoaded) {
+          const fetchOptions = options as SelectOptionsPagePromise;
+          option = await fetchOptions(text, 0, pageSize).then(
+            ({ data }: SelectOptionsTypePage) =>
+              data.find(item => item.label === text),
+          );
+        }
         const value: AntdLabeledValue = {
           label: text,
           value: text,
@@ -546,20 +553,22 @@ const AsyncSelect = forwardRef(
         }
         return value;
       },
-      [fullSelectOptions],
+      [allValuesLoaded, fullSelectOptions, options, pageSize],
     );
 
-    const onPaste = (e: ClipboardEvent<HTMLInputElement>) => {
+    const onPaste = async (e: ClipboardEvent<HTMLInputElement>) => {
       const pastedText = e.clipboardData.getData('text');
       if (isSingleMode) {
-        setSelectValue(getPastedTextValue(pastedText));
+        setSelectValue(await getPastedTextValue(pastedText));
       } else {
         const token = tokenSeparators.find(token => pastedText.includes(token));
         const array = token ? uniq(pastedText.split(token)) : [pastedText];
-        const values = array.map(item => getPastedTextValue(item));
+        const values = await Promise.all(
+          array.map(item => getPastedTextValue(item)),
+        );
         setSelectValue(previous => [
           ...((previous || []) as AntdLabeledValue[]),
-          ...values,
+          ...values.filter(value => !hasOption(value.value, previous)),
         ]);
       }
       fireOnChange();