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/04/18 19:31:17 UTC

[superset] branch master updated: feat: Makes "Add to dashboard" in Save chart modal paginated (#23634)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new d6b6d9eae6 feat: Makes "Add to dashboard" in Save chart modal paginated (#23634)
d6b6d9eae6 is described below

commit d6b6d9eae654d7d57a20b9c52d9b9b956627877a
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Tue Apr 18 16:31:09 2023 -0300

    feat: Makes "Add to dashboard" in Save chart modal paginated (#23634)
---
 .../cypress/integration/dashboard/utils.ts         |   4 -
 .../cypress/integration/explore/utils.ts           |   7 +-
 .../src/explore/actions/saveModalActions.js        |  37 ---
 .../src/explore/actions/saveModalActions.test.js   |  68 -----
 .../src/explore/components/SaveModal.test.jsx      |  24 +-
 .../src/explore/components/SaveModal.tsx           | 294 ++++++++++++---------
 6 files changed, 170 insertions(+), 264 deletions(-)

diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts
index 0b8776b06c..44322e1c42 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts
@@ -163,10 +163,6 @@ export function interceptDatasets() {
   cy.intercept('GET', `/api/v1/dashboard/*/datasets`).as('getDatasets');
 }
 
-export function interceptDashboardasync() {
-  cy.intercept('GET', `/dashboardasync/api/read*`).as('getDashboardasync');
-}
-
 export function interceptFilterState() {
   cy.intercept('POST', `/api/v1/dashboard/*/filter_state*`).as(
     'postFilterState',
diff --git a/superset-frontend/cypress-base/cypress/integration/explore/utils.ts b/superset-frontend/cypress-base/cypress/integration/explore/utils.ts
index 15e7dcba1b..04cf1f1819 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/utils.ts
+++ b/superset-frontend/cypress-base/cypress/integration/explore/utils.ts
@@ -17,10 +17,7 @@
  * under the License.
  */
 
-import {
-  interceptGet as interceptDashboardGet,
-  interceptDashboardasync,
-} from '../dashboard/utils';
+import { interceptGet as interceptDashboardGet } from '../dashboard/utils';
 
 export function interceptFiltering() {
   cy.intercept('GET', `/api/v1/chart/?q=*`).as('filtering');
@@ -61,12 +58,10 @@ export function setFilter(filter: string, option: string) {
 
 export function saveChartToDashboard(dashboardName: string) {
   interceptDashboardGet();
-  interceptDashboardasync();
   interceptUpdate();
   interceptExploreGet();
 
   cy.getBySel('query-save-button').click();
-  cy.wait('@getDashboardasync');
   cy.getBySelLike('chart-modal').should('be.visible');
   cy.get(
     '[data-test="save-chart-modal-select-dashboard-form"] [aria-label="Select a dashboard"]',
diff --git a/superset-frontend/src/explore/actions/saveModalActions.js b/superset-frontend/src/explore/actions/saveModalActions.js
index 1c3c3b765f..9bd5161391 100644
--- a/superset-frontend/src/explore/actions/saveModalActions.js
+++ b/superset-frontend/src/explore/actions/saveModalActions.js
@@ -40,29 +40,6 @@ export function setSaveChartModalVisibility(isVisible) {
   return { type: SET_SAVE_CHART_MODAL_VISIBILITY, isVisible };
 }
 
-export function fetchDashboards(userId) {
-  return function fetchDashboardsThunk(dispatch) {
-    return SupersetClient.get({
-      endpoint: `/dashboardasync/api/read?_flt_0_owners=${userId}`,
-    })
-      .then(({ json }) => {
-        const choices = json.pks.map((id, index) => ({
-          value: id,
-          label: (json.result[index] || {}).dashboard_title,
-        }));
-        choices.sort((a, b) =>
-          a.label.localeCompare(b.label, {
-            sensitivity: 'base',
-            numeric: true,
-          }),
-        );
-
-        return dispatch(fetchDashboardsSucceeded(choices));
-      })
-      .catch(() => dispatch(fetchDashboardsFailed(userId)));
-  };
-}
-
 export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED';
 export function saveSliceFailed() {
   return { type: SAVE_SLICE_FAILED };
@@ -241,20 +218,6 @@ export const createDashboard = dashboardName => async dispatch => {
   }
 };
 
-//  Get existing dashboard from ID
-export const getDashboard = dashboardId => async dispatch => {
-  try {
-    const response = await SupersetClient.get({
-      endpoint: `/api/v1/dashboard/${dashboardId}`,
-    });
-
-    return response.json;
-  } catch (error) {
-    dispatch(saveSliceFailed());
-    throw error;
-  }
-};
-
 //  Get dashboards the slice is added to
 export const getSliceDashboards = slice => async dispatch => {
   try {
diff --git a/superset-frontend/src/explore/actions/saveModalActions.test.js b/superset-frontend/src/explore/actions/saveModalActions.test.js
index f89729f5ff..1662ead63e 100644
--- a/superset-frontend/src/explore/actions/saveModalActions.test.js
+++ b/superset-frontend/src/explore/actions/saveModalActions.test.js
@@ -23,10 +23,6 @@ import { ADD_TOAST } from 'src/components/MessageToasts/actions';
 import {
   createDashboard,
   createSlice,
-  fetchDashboards,
-  FETCH_DASHBOARDS_FAILED,
-  FETCH_DASHBOARDS_SUCCEEDED,
-  getDashboard,
   getSliceDashboards,
   SAVE_SLICE_FAILED,
   SAVE_SLICE_SUCCESS,
@@ -34,37 +30,6 @@ import {
   getSlicePayload,
 } from './saveModalActions';
 
-/**
- * Tests fetchDashboards action
- */
-
-const userId = 1;
-const fetchDashboardsEndpoint = `glob:*/dashboardasync/api/read?_flt_0_owners=${1}`;
-const mockDashboardData = {
-  pks: ['id'],
-  result: [{ id: 'id', dashboard_title: 'dashboard title' }],
-};
-
-test('fetchDashboards handles success', async () => {
-  fetchMock.reset();
-  fetchMock.get(fetchDashboardsEndpoint, mockDashboardData);
-  const dispatch = sinon.spy();
-  await fetchDashboards(userId)(dispatch);
-  expect(fetchMock.calls(fetchDashboardsEndpoint)).toHaveLength(1);
-  expect(dispatch.callCount).toBe(1);
-  expect(dispatch.getCall(0).args[0].type).toBe(FETCH_DASHBOARDS_SUCCEEDED);
-});
-
-test('fetchDashboards handles failure', async () => {
-  fetchMock.reset();
-  fetchMock.get(fetchDashboardsEndpoint, { throws: 'error' });
-  const dispatch = sinon.spy();
-  await fetchDashboards(userId)(dispatch);
-  expect(fetchMock.calls(fetchDashboardsEndpoint)).toHaveLength(4); // 3 retries
-  expect(dispatch.callCount).toBe(1);
-  expect(dispatch.getCall(0).args[0].type).toBe(FETCH_DASHBOARDS_FAILED);
-});
-
 const sliceId = 10;
 const sliceName = 'New chart';
 const vizType = 'sample_viz_type';
@@ -176,7 +141,6 @@ test('createSlice handles failure', async () => {
   expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED);
 });
 
-const dashboardId = 14;
 const dashboardName = 'New dashboard';
 const dashboardResponsePayload = {
   id: 14,
@@ -214,38 +178,6 @@ test('createDashboard handles failure', async () => {
   expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED);
 });
 
-/**
- * Tests getDashboard action
- */
-
-const getDashboardEndpoint = `glob:*/api/v1/dashboard/${dashboardId}`;
-test('getDashboard handles success', async () => {
-  fetchMock.reset();
-  fetchMock.get(getDashboardEndpoint, dashboardResponsePayload);
-  const dispatch = sinon.spy();
-  const dashboard = await getDashboard(dashboardId)(dispatch);
-  expect(fetchMock.calls(getDashboardEndpoint)).toHaveLength(1);
-  expect(dispatch.callCount).toBe(0);
-  expect(dashboard).toEqual(dashboardResponsePayload);
-});
-
-test('getDashboard handles failure', async () => {
-  fetchMock.reset();
-  fetchMock.get(getDashboardEndpoint, { throws: sampleError });
-  const dispatch = sinon.spy();
-  let caughtError;
-  try {
-    await getDashboard(dashboardId)(dispatch);
-  } catch (error) {
-    caughtError = error;
-  }
-
-  expect(caughtError).toEqual(sampleError);
-  expect(fetchMock.calls(getDashboardEndpoint)).toHaveLength(4);
-  expect(dispatch.callCount).toBe(1);
-  expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED);
-});
-
 test('updateSlice with add to new dashboard handles success', async () => {
   fetchMock.reset();
   fetchMock.put(updateSliceEndpoint, sliceResponsePayload);
diff --git a/superset-frontend/src/explore/components/SaveModal.test.jsx b/superset-frontend/src/explore/components/SaveModal.test.jsx
index 15bfc64e75..74d1c1199c 100644
--- a/superset-frontend/src/explore/components/SaveModal.test.jsx
+++ b/superset-frontend/src/explore/components/SaveModal.test.jsx
@@ -20,10 +20,8 @@ import React from 'react';
 import configureStore from 'redux-mock-store';
 import thunk from 'redux-thunk';
 import { bindActionCreators } from 'redux';
-import { Provider } from 'react-redux';
 
 import { shallow } from 'enzyme';
-import { styledMount as mount } from 'spec/helpers/theming';
 import { Radio } from 'src/components/Radio';
 import Button from 'src/components/Button';
 import sinon from 'sinon';
@@ -39,6 +37,7 @@ const initialState = {
   chart: {},
   saveModal: {
     dashboards: [],
+    isVisible: true,
   },
   explore: {
     datasource: {},
@@ -57,6 +56,7 @@ const initialState = {
 const initialStore = mockStore(initialState);
 
 const defaultProps = {
+  addDangerToast: jest.fn(),
   onHide: () => ({}),
   actions: bindActionCreators(saveModalActions, arg => {
     if (typeof arg === 'function') {
@@ -83,6 +83,7 @@ const queryStore = mockStore({
   chart: {},
   saveModal: {
     dashboards: [],
+    isVisible: true,
   },
   explore: {
     datasource: { name: 'test', type: 'query' },
@@ -144,8 +145,7 @@ test('renders the right footer buttons when existing dashboard selected', () =>
 test('renders the right footer buttons when new dashboard selected', () => {
   const wrapper = getWrapper();
   wrapper.setState({
-    saveToDashboardId: null,
-    newDashboardName: 'Test new dashboard',
+    dashboard: { label: 'Test new dashboard', value: 'Test new dashboard' },
   });
   const footerWrapper = shallow(wrapper.find(StyledModal).props().footer);
   const saveAndGoDash = footerWrapper
@@ -186,18 +186,6 @@ test('sets action when overwriting slice', () => {
   expect(wrapperForOverwrite.state().action).toBe('overwrite');
 });
 
-test('fetches dashboards on component mount', () => {
-  sinon.spy(defaultProps.actions, 'fetchDashboards');
-  mount(
-    <Provider store={initialStore}>
-      <SaveModal {...defaultProps} />
-    </Provider>,
-  );
-  expect(defaultProps.actions.fetchDashboards.calledOnce).toBe(true);
-
-  defaultProps.actions.fetchDashboards.restore();
-});
-
 test('updates slice name and selected dashboard', () => {
   const wrapper = getWrapper();
   const dashboardId = mockEvent.value;
@@ -205,8 +193,8 @@ test('updates slice name and selected dashboard', () => {
   wrapper.instance().onSliceNameChange(mockEvent);
   expect(wrapper.state().newSliceName).toBe(mockEvent.target.value);
 
-  wrapper.instance().onDashboardSelectChange(dashboardId);
-  expect(wrapper.state().saveToDashboardId).toBe(dashboardId);
+  wrapper.instance().onDashboardChange({ value: dashboardId });
+  expect(wrapper.state().dashboard.value).toBe(dashboardId);
 });
 
 test('removes alert', () => {
diff --git a/superset-frontend/src/explore/components/SaveModal.tsx b/superset-frontend/src/explore/components/SaveModal.tsx
index 9e63f10b61..849baa1417 100644
--- a/superset-frontend/src/explore/components/SaveModal.tsx
+++ b/superset-frontend/src/explore/components/SaveModal.tsx
@@ -19,17 +19,18 @@
 /* eslint camelcase: 0 */
 import React from 'react';
 import { Dispatch } from 'redux';
-import { SelectValue } from 'antd/lib/select';
+import { isFeatureEnabled } from 'src/featureFlags';
+import rison from 'rison';
 import { connect } from 'react-redux';
 import { withRouter, RouteComponentProps } from 'react-router-dom';
 import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
 import {
   css,
   DatasourceType,
-  ensureIsArray,
   FeatureFlag,
   isDefined,
   styled,
+  SupersetClient,
   t,
 } from '@superset-ui/core';
 import { Input } from 'src/components/Input';
@@ -38,11 +39,10 @@ import Alert from 'src/components/Alert';
 import Modal from 'src/components/Modal';
 import { Radio } from 'src/components/Radio';
 import Button from 'src/components/Button';
-import { Select } from 'src/components';
+import { AsyncSelect } from 'src/components';
 import Loading from 'src/components/Loading';
 import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
 import { SaveActionType } from 'src/explore/types';
-import { isFeatureEnabled } from 'src/featureFlags';
 
 // Session storage key for recent dashboard
 const SK_DASHBOARD_ID = 'save_chart_recent_dashboard';
@@ -52,7 +52,6 @@ interface SaveModalProps extends RouteComponentProps {
   actions: Record<string, any>;
   form_data?: Record<string, any>;
   userId: number;
-  dashboards: Array<any>;
   alert?: string;
   sliceName?: string;
   slice?: Record<string, any>;
@@ -63,15 +62,14 @@ interface SaveModalProps extends RouteComponentProps {
 }
 
 type SaveModalState = {
-  saveToDashboardId: number | string | null;
   newSliceName?: string;
-  newDashboardName?: string;
   datasetName: string;
   alert: string | null;
   action: SaveActionType;
   isLoading: boolean;
   saveStatus?: string | null;
   vizType?: string;
+  dashboard?: { label: string; value: string | number };
 };
 
 export const StyledModal = styled(Modal)`
@@ -89,15 +87,15 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
   constructor(props: SaveModalProps) {
     super(props);
     this.state = {
-      saveToDashboardId: null,
       newSliceName: props.sliceName,
       datasetName: props.datasource?.name,
       alert: null,
       action: this.canOverwriteSlice() ? 'overwrite' : 'saveas',
       isLoading: false,
       vizType: props.form_data?.viz_type,
+      dashboard: undefined,
     };
-    this.onDashboardSelectChange = this.onDashboardSelectChange.bind(this);
+    this.onDashboardChange = this.onDashboardChange.bind(this);
     this.onSliceNameChange = this.onSliceNameChange.bind(this);
     this.changeAction = this.changeAction.bind(this);
     this.saveOrOverwrite = this.saveOrOverwrite.bind(this);
@@ -107,7 +105,8 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
   }
 
   isNewDashboard(): boolean {
-    return !!(!this.state.saveToDashboardId && this.state.newDashboardName);
+    const { dashboard } = this.state;
+    return typeof dashboard?.value === 'string';
   }
 
   canOverwriteSlice(): boolean {
@@ -117,30 +116,26 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
     );
   }
 
-  componentDidMount() {
-    this.props.actions.fetchDashboards(this.props.userId).then(() => {
-      if (ensureIsArray(this.props.dashboards).length === 0) {
-        return;
-      }
-      const dashboardIds = this.props.dashboards?.map(
-        dashboard => dashboard.value,
-      );
+  async componentDidMount() {
+    let { dashboardId } = this.props;
+    if (!dashboardId) {
       const lastDashboard = sessionStorage.getItem(SK_DASHBOARD_ID);
-      let recentDashboard = lastDashboard && parseInt(lastDashboard, 10);
-
-      if (this.props.dashboardId) {
-        recentDashboard = this.props.dashboardId;
-      }
-
-      if (
-        recentDashboard !== null &&
-        dashboardIds.indexOf(recentDashboard) !== -1
-      ) {
-        this.setState({
-          saveToDashboardId: recentDashboard,
-        });
+      dashboardId = lastDashboard && parseInt(lastDashboard, 10);
+    }
+    if (dashboardId) {
+      try {
+        const result = await this.loadDashboard(dashboardId);
+        if (result) {
+          this.setState({
+            dashboard: { label: result.dashboard_title, value: result.id },
+          });
+        }
+      } catch (error) {
+        this.props.actions.addDangerToast(
+          t('An error occurred while loading dashboard information.'),
+        );
       }
-    });
+    }
   }
 
   handleDatasetNameChange = (e: React.FormEvent<HTMLInputElement>) => {
@@ -152,11 +147,8 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
     this.setState({ newSliceName: event.target.value });
   }
 
-  onDashboardSelectChange(selected: SelectValue) {
-    const newDashboardName = selected ? String(selected) : undefined;
-    const saveToDashboardId =
-      selected && typeof selected === 'number' ? selected : null;
-    this.setState({ saveToDashboardId, newDashboardName });
+  onDashboardChange(dashboard: { label: string; value: string | number }) {
+    this.setState({ dashboard });
   }
 
   changeAction(action: SaveActionType) {
@@ -206,19 +198,22 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
       delete formData.url_params;
 
       let dashboard: DashboardGetResponse | null = null;
-      if (this.state.newDashboardName || this.state.saveToDashboardId) {
-        let saveToDashboardId = this.state.saveToDashboardId || null;
-        if (!this.state.saveToDashboardId) {
+      if (this.state.dashboard) {
+        let validId = this.state.dashboard.value;
+        if (this.isNewDashboard()) {
           const response = await this.props.actions.createDashboard(
-            this.state.newDashboardName,
+            this.state.dashboard.label,
           );
-          saveToDashboardId = response.id;
+          validId = response.id;
+        }
+
+        try {
+          dashboard = await this.loadDashboard(validId as number);
+        } catch (error) {
+          this.props.actions.saveSliceFailed();
+          return;
         }
 
-        const response = await this.props.actions.getDashboard(
-          saveToDashboardId,
-        );
-        dashboard = response.result;
         if (isDefined(dashboard) && isDefined(dashboard?.id)) {
           sliceDashboards = sliceDashboards.includes(dashboard.id)
             ? sliceDashboards
@@ -240,7 +235,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
           dashboard
             ? {
                 title: dashboard.dashboard_title,
-                new: !this.state.saveToDashboardId,
+                new: this.isNewDashboard(),
               }
             : null,
         );
@@ -251,7 +246,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
           dashboard
             ? {
                 title: dashboard.dashboard_title,
-                new: !this.state.saveToDashboardId,
+                new: this.isNewDashboard(),
               }
             : null,
         );
@@ -284,94 +279,131 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
     }
   }
 
-  renderSaveChartModal = () => {
-    const dashboardSelectValue =
-      this.state.saveToDashboardId || this.state.newDashboardName;
+  loadDashboard = async (id: number) => {
+    const response = await SupersetClient.get({
+      endpoint: `/api/v1/dashboard/${id}`,
+    });
+    return response.json.result;
+  };
 
-    return (
-      <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}
+  loadDashboards = async (search: string, page: number, pageSize: number) => {
+    const queryParams = rison.encode({
+      columns: ['id', 'dashboard_title'],
+      filters: [
+        {
+          col: 'dashboard_title',
+          opr: 'ct',
+          value: search,
+        },
+        {
+          col: 'owners',
+          opr: 'rel_m_m',
+          value: this.props.userId,
+        },
+      ],
+      page,
+      page_size: pageSize,
+      order_column: 'dashboard_title',
+    });
+
+    const { json } = await SupersetClient.get({
+      endpoint: `/api/v1/dashboard/?q=${queryParams}`,
+    });
+    const { result, count } = json;
+    return {
+      data: result.map(
+        (dashboard: { id: number; dashboard_title: string }) => ({
+          value: dashboard.id,
+          label: dashboard.dashboard_title,
+        }),
+      ),
+      totalCount: count,
+    };
+  };
+
+  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"
           />
-        )}
-        <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"
+            name="dataset_name"
             type="text"
-            placeholder="Name"
-            value={this.state.newSliceName}
-            onChange={this.onSliceNameChange}
-            data-test="new-chart-name"
+            placeholder="Dataset Name"
+            value={this.state.datasetName}
+            onChange={this.handleDatasetNameChange}
+            data-test="new-dataset-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"
-            />
-            <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"
-          >
-            <Select
-              allowClear
-              allowNewOptions
-              ariaLabel={t('Select a dashboard')}
-              options={this.props.dashboards}
-              onChange={this.onDashboardSelectChange}
-              value={dashboardSelectValue || undefined}
-              placeholder={
-                <div>
-                  <b>{t('Select')}</b>
-                  {t(' a dashboard OR ')}
-                  <b>{t('create')}</b>
-                  {t(' a new one')}
-                </div>
-              }
-            />
-          </FormItem>
-        )}
-      </Form>
-    );
-  };
+      )}
+      {!(
+        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>
+      )}
+    </Form>
+  );
 
   renderFooter = () => (
     <div data-test="save-modal-footer">
@@ -383,7 +415,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
         buttonSize="small"
         disabled={
           !this.state.newSliceName ||
-          (!this.state.saveToDashboardId && !this.state.newDashboardName) ||
+          !this.state.dashboard ||
           (this.props.datasource?.type !== DatasourceType.Table &&
             !this.state.datasetName) ||
           (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&