You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by yj...@apache.org on 2020/10/25 04:41:06 UTC

[incubator-superset] branch master updated: refactor: typing for explore Control and messageToasts (#11416)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 8aecffd  refactor: typing for explore Control and messageToasts (#11416)
8aecffd is described below

commit 8aecffd83ba994c73d10c6879f9ab274cef11153
Author: Jesse Yang <je...@airbnb.com>
AuthorDate: Sat Oct 24 21:40:36 2020 -0700

    refactor: typing for explore Control and messageToasts (#11416)
---
 .../dashboard/actions/dashboardLayout_spec.js      | 29 ++++---
 .../javascripts/messageToasts/mockMessageToasts.js |  6 +-
 .../utils/getToastsFromPyFlashMessages_spec.js     | 15 ++--
 superset-frontend/src/chart/Chart.jsx              |  4 +-
 .../src/datasource/DatasourceEditor.jsx            |  1 +
 .../{exploreActions.js => exploreActions.ts}       | 91 +++++++++++++---------
 .../components/{Control.jsx => Control.tsx}        | 76 ++++++++----------
 .../explore/components/ControlPanelsContainer.jsx  |  4 +-
 .../src/explore/components/PropertiesModal.tsx     | 51 ++++++------
 .../src/explore/reducers/exploreReducer.js         | 13 ----
 .../messageToasts/actions/{index.js => index.ts}   | 38 ++++-----
 .../src/messageToasts/components/Toast.tsx         | 39 +++++-----
 .../messageToasts/components/ToastPresenter.tsx    | 37 ++++-----
 .../messageToasts/{constants.js => constants.ts}   | 13 +++-
 superset-frontend/src/messageToasts/propShapes.js  | 39 ----------
 superset-frontend/src/messageToasts/types.ts       | 17 ++--
 .../utils/getToastsFromPyFlashMessages.js          |  6 +-
 superset-frontend/src/types/Chart.ts               |  8 ++
 .../src/views/CRUD/chart/ChartList.tsx             |  4 +-
 19 files changed, 226 insertions(+), 265 deletions(-)

diff --git a/superset-frontend/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset-frontend/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
index 6f072fe..4a98667 100644
--- a/superset-frontend/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
+++ b/superset-frontend/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
@@ -40,7 +40,7 @@ import {
 
 import { setUnsavedChanges } from 'src/dashboard/actions/dashboardState';
 import * as dashboardFilters from 'src/dashboard/actions/dashboardFilters';
-import { addWarningToast, ADD_TOAST } from 'src/messageToasts/actions';
+import { ADD_TOAST } from 'src/messageToasts/actions';
 
 import {
   DASHBOARD_GRID_TYPE,
@@ -349,24 +349,27 @@ describe('dashboardLayout actions', () => {
       const { getState, dispatch } = setup({
         dashboardLayout: {
           present: {
-            source: { type: ROW_TYPE },
-            destination: { type: ROW_TYPE, children: ['rowChild'] },
-            dragging: { type: CHART_TYPE, meta: { width: 1 } },
-            rowChild: { type: CHART_TYPE, meta: { width: 12 } },
+            source: { id: 'source', type: ROW_TYPE, children: ['dragging'] },
+            destination: {
+              id: 'destination',
+              type: ROW_TYPE,
+              children: ['rowChild'],
+            },
+            dragging: { id: 'dragging', type: CHART_TYPE, meta: { width: 1 } },
+            rowChild: { id: 'rowChild', type: CHART_TYPE, meta: { width: 12 } },
           },
         },
       });
       const dropResult = {
         source: { id: 'source', type: ROW_TYPE },
         destination: { id: 'destination', type: ROW_TYPE },
-        dragging: { id: 'dragging', type: CHART_TYPE },
+        dragging: { id: 'dragging', type: CHART_TYPE, meta: { width: 1 } },
       };
 
       const thunk = handleComponentDrop(dropResult);
       thunk(dispatch, getState);
-      expect(dispatch.getCall(0).args[0].type).toEqual(
-        addWarningToast('').type,
-      );
+
+      expect(dispatch.getCall(0).args[0].type).toEqual(ADD_TOAST);
 
       expect(dispatch.callCount).toBe(1);
     });
@@ -479,13 +482,9 @@ describe('dashboardLayout actions', () => {
         },
       };
 
-      const thunk1 = handleComponentDrop(dropResult);
-      thunk1(dispatch, getState);
-
-      const thunk2 = dispatch.getCall(0).args[0];
-      thunk2(dispatch, getState);
+      handleComponentDrop(dropResult)(dispatch, getState);
 
-      expect(dispatch.getCall(1).args[0].type).toEqual(ADD_TOAST);
+      expect(dispatch.getCall(0).args[0].type).toEqual(ADD_TOAST);
     });
   });
 
diff --git a/superset-frontend/spec/javascripts/messageToasts/mockMessageToasts.js b/superset-frontend/spec/javascripts/messageToasts/mockMessageToasts.js
index e009ba5..311e626 100644
--- a/superset-frontend/spec/javascripts/messageToasts/mockMessageToasts.js
+++ b/superset-frontend/spec/javascripts/messageToasts/mockMessageToasts.js
@@ -16,9 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { INFO_TOAST, DANGER_TOAST } from 'src/messageToasts/constants';
+import { ToastType } from 'src/messageToasts/constants';
 
 export default [
-  { id: 'info_id', toastType: INFO_TOAST, text: 'info toast' },
-  { id: 'danger_id', toastType: DANGER_TOAST, text: 'danger toast' },
+  { id: 'info_id', toastType: ToastType.INFO, text: 'info toast' },
+  { id: 'danger_id', toastType: ToastType.DANGER, text: 'danger toast' },
 ];
diff --git a/superset-frontend/spec/javascripts/messageToasts/utils/getToastsFromPyFlashMessages_spec.js b/superset-frontend/spec/javascripts/messageToasts/utils/getToastsFromPyFlashMessages_spec.js
index edabc0a..59f8a45 100644
--- a/superset-frontend/spec/javascripts/messageToasts/utils/getToastsFromPyFlashMessages_spec.js
+++ b/superset-frontend/spec/javascripts/messageToasts/utils/getToastsFromPyFlashMessages_spec.js
@@ -16,18 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import {
-  DANGER_TOAST,
-  INFO_TOAST,
-  SUCCESS_TOAST,
-} from 'src/messageToasts/constants';
+import { ToastType } from 'src/messageToasts/constants';
 
 import getToastsFromPyFlashMessages from 'src/messageToasts/utils/getToastsFromPyFlashMessages';
 
 describe('getToastsFromPyFlashMessages', () => {
   it('should return an info toast', () => {
     const toast = getToastsFromPyFlashMessages([['info', 'info test']])[0];
-    expect(toast).toMatchObject({ toastType: INFO_TOAST, text: 'info test' });
+    expect(toast).toMatchObject({
+      toastType: ToastType.INFO,
+      text: 'info test',
+    });
   });
 
   it('should return a success toast', () => {
@@ -35,7 +34,7 @@ describe('getToastsFromPyFlashMessages', () => {
       ['success', 'success test'],
     ])[0];
     expect(toast).toMatchObject({
-      toastType: SUCCESS_TOAST,
+      toastType: ToastType.SUCCESS,
       text: 'success test',
     });
   });
@@ -43,7 +42,7 @@ describe('getToastsFromPyFlashMessages', () => {
   it('should return a danger toast', () => {
     const toast = getToastsFromPyFlashMessages([['danger', 'danger test']])[0];
     expect(toast).toMatchObject({
-      toastType: DANGER_TOAST,
+      toastType: ToastType.DANGER,
       text: 'danger test',
     });
   });
diff --git a/superset-frontend/src/chart/Chart.jsx b/superset-frontend/src/chart/Chart.jsx
index 41f2785..1d386ec 100644
--- a/superset-frontend/src/chart/Chart.jsx
+++ b/superset-frontend/src/chart/Chart.jsx
@@ -49,7 +49,9 @@ const propTypes = {
   timeout: PropTypes.number,
   vizType: PropTypes.string.isRequired,
   triggerRender: PropTypes.bool,
-  owners: PropTypes.arrayOf(PropTypes.string),
+  owners: PropTypes.arrayOf(
+    PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  ),
   // state
   chartAlert: PropTypes.string,
   chartStatus: PropTypes.string,
diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx
index 01684b1..3cfba94 100644
--- a/superset-frontend/src/datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/datasource/DatasourceEditor.jsx
@@ -618,6 +618,7 @@ class DatasourceEditor extends React.PureComponent {
         <div className="m-l-10 m-t-20 m-b-10">
           {DATASOURCE_TYPES_ARR.map(type => (
             <Radio
+              key={type.key}
               value={type.key}
               inline
               onChange={this.onDatasourceTypeChange.bind(this, type.key)}
diff --git a/superset-frontend/src/explore/actions/exploreActions.js b/superset-frontend/src/explore/actions/exploreActions.ts
similarity index 67%
rename from superset-frontend/src/explore/actions/exploreActions.js
rename to superset-frontend/src/explore/actions/exploreActions.ts
index 56c112b..af71199 100644
--- a/superset-frontend/src/explore/actions/exploreActions.js
+++ b/superset-frontend/src/explore/actions/exploreActions.ts
@@ -17,23 +17,31 @@
  * under the License.
  */
 /* eslint camelcase: 0 */
-import { t, SupersetClient } from '@superset-ui/core';
-import { addDangerToast } from '../../messageToasts/actions';
+import { DatasourceMeta } from '@superset-ui/chart-controls';
+import {
+  t,
+  SupersetClient,
+  DatasourceType,
+  QueryFormData,
+} from '@superset-ui/core';
+import { Dispatch } from 'redux';
+import { addDangerToast } from 'src/messageToasts/actions';
+import { Slice } from 'src/types/Chart';
 
 const FAVESTAR_BASE_URL = '/superset/favstar/slice';
 
 export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE';
-export function setDatasourceType(datasourceType) {
+export function setDatasourceType(datasourceType: DatasourceType) {
   return { type: SET_DATASOURCE_TYPE, datasourceType };
 }
 
 export const SET_DATASOURCE = 'SET_DATASOURCE';
-export function setDatasource(datasource) {
+export function setDatasource(datasource: DatasourceMeta) {
   return { type: SET_DATASOURCE, datasource };
 }
 
 export const SET_DATASOURCES = 'SET_DATASOURCES';
-export function setDatasources(datasources) {
+export function setDatasources(datasources: DatasourceMeta[]) {
   return { type: SET_DATASOURCES, datasources };
 }
 
@@ -53,29 +61,19 @@ export function fetchDatasourcesSucceeded() {
   return { type: FETCH_DATASOURCES_SUCCEEDED };
 }
 
-export const FETCH_DATASOURCES_FAILED = 'FETCH_DATASOURCES_FAILED';
-export function fetchDatasourcesFailed(error) {
-  return { type: FETCH_DATASOURCES_FAILED, error };
-}
-
-export const POST_DATASOURCES_FAILED = 'POST_DATASOURCES_FAILED';
-export function postDatasourcesFailed(error) {
-  return { type: POST_DATASOURCES_FAILED, error };
-}
-
 export const RESET_FIELDS = 'RESET_FIELDS';
 export function resetControls() {
   return { type: RESET_FIELDS };
 }
 
 export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
-export function toggleFaveStar(isStarred) {
+export function toggleFaveStar(isStarred: boolean) {
   return { type: TOGGLE_FAVE_STAR, isStarred };
 }
 
 export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
-export function fetchFaveStar(sliceId) {
-  return function (dispatch) {
+export function fetchFaveStar(sliceId: string) {
+  return function (dispatch: Dispatch<ReturnType<typeof toggleFaveStar>>) {
     SupersetClient.get({
       endpoint: `${FAVESTAR_BASE_URL}/${sliceId}/count`,
     }).then(({ json }) => {
@@ -87,33 +85,32 @@ export function fetchFaveStar(sliceId) {
 }
 
 export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
-export function saveFaveStar(sliceId, isStarred) {
-  return function (dispatch) {
+export function saveFaveStar(sliceId: string, isStarred: boolean) {
+  return function (dispatch: Dispatch<ReturnType<typeof addDangerToast>>) {
     const urlSuffix = isStarred ? 'unselect' : 'select';
     SupersetClient.get({
       endpoint: `${FAVESTAR_BASE_URL}/${sliceId}/${urlSuffix}/`,
     })
       .then(() => dispatch(toggleFaveStar(!isStarred)))
-      .catch(() =>
+      .catch(() => {
         dispatch(
           addDangerToast(t('An error occurred while starring this chart')),
-        ),
-      );
+        );
+      });
   };
 }
 
 export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
-export function setControlValue(controlName, value, validationErrors) {
+export function setControlValue(
+  controlName: string,
+  value: any,
+  validationErrors: any[],
+) {
   return { type: SET_FIELD_VALUE, controlName, value, validationErrors };
 }
 
-export const UPDATE_EXPLORE_ENDPOINTS = 'UPDATE_EXPLORE_ENDPOINTS';
-export function updateExploreEndpoints(jsonUrl, csvUrl, standaloneUrl) {
-  return { type: UPDATE_EXPLORE_ENDPOINTS, jsonUrl, csvUrl, standaloneUrl };
-}
-
 export const SET_EXPLORE_CONTROLS = 'UPDATE_EXPLORE_CONTROLS';
-export function setExploreControls(formData) {
+export function setExploreControls(formData: QueryFormData) {
   return { type: SET_EXPLORE_CONTROLS, formData };
 }
 
@@ -123,17 +120,17 @@ export function removeControlPanelAlert() {
 }
 
 export const UPDATE_CHART_TITLE = 'UPDATE_CHART_TITLE';
-export function updateChartTitle(sliceName) {
+export function updateChartTitle(sliceName: string) {
   return { type: UPDATE_CHART_TITLE, sliceName };
 }
 
 export const CREATE_NEW_SLICE = 'CREATE_NEW_SLICE';
 export function createNewSlice(
-  can_add,
-  can_download,
-  can_overwrite,
-  slice,
-  form_data,
+  can_add: boolean,
+  can_download: boolean,
+  can_overwrite: boolean,
+  slice: Slice,
+  form_data: QueryFormData,
 ) {
   return {
     type: CREATE_NEW_SLICE,
@@ -146,6 +143,26 @@ export function createNewSlice(
 }
 
 export const SLICE_UPDATED = 'SLICE_UPDATED';
-export function sliceUpdated(slice) {
+export function sliceUpdated(slice: Slice) {
   return { type: SLICE_UPDATED, slice };
 }
+
+export const exploreActions = {
+  setDatasourceType,
+  setDatasource,
+  setDatasources,
+  fetchDatasourcesStarted,
+  fetchDatasourcesSucceeded,
+  resetControls,
+  toggleFaveStar,
+  fetchFaveStar,
+  saveFaveStar,
+  setControlValue,
+  setExploreControls,
+  removeControlPanelAlert,
+  updateChartTitle,
+  createNewSlice,
+  sliceUpdated,
+};
+
+export type ExploreActions = typeof exploreActions;
diff --git a/superset-frontend/src/explore/components/Control.jsx b/superset-frontend/src/explore/components/Control.tsx
similarity index 60%
rename from superset-frontend/src/explore/components/Control.jsx
rename to superset-frontend/src/explore/components/Control.tsx
index 68f0e65..8fc6583 100644
--- a/superset-frontend/src/explore/components/Control.jsx
+++ b/superset-frontend/src/explore/components/Control.tsx
@@ -16,52 +16,41 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import './Control.less';
+import React, { ReactNode } from 'react';
+import { ControlType } from '@superset-ui/chart-controls';
+import { JsonValue, QueryFormData } from '@superset-ui/core';
+import { ExploreActions } from '../actions/exploreActions';
 import controlMap from './controls';
 
-const controlTypes = Object.keys(controlMap);
+import './Control.less';
 
-const propTypes = {
-  actions: PropTypes.object.isRequired,
-  name: PropTypes.string.isRequired,
-  type: PropTypes.oneOfType([
-    PropTypes.oneOf(controlTypes).isRequired,
-    PropTypes.func.isRequired,
-  ]),
-  hidden: PropTypes.bool,
-  label: PropTypes.string.isRequired,
-  choices: PropTypes.oneOfType([
-    PropTypes.arrayOf(PropTypes.array),
-    PropTypes.func,
-  ]),
-  description: PropTypes.string,
-  tooltipOnClick: PropTypes.func,
-  places: PropTypes.number,
-  validationErrors: PropTypes.array,
-  renderTrigger: PropTypes.bool,
-  rightNode: PropTypes.node,
-  formData: PropTypes.object,
-  value: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.number,
-    PropTypes.object,
-    PropTypes.bool,
-    PropTypes.array,
-    PropTypes.func,
-  ]),
+export type ControlProps = {
+  // the actual action dispatcher (via bindActionCreators) has identical
+  // signature to the original action factory.
+  actions: ExploreActions;
+  type: ControlType;
+  label: string;
+  name: string;
+  description?: string;
+  tooltipOnClick?: () => ReactNode;
+  places?: number;
+  rightNode?: ReactNode;
+  formData?: QueryFormData | null;
+  value?: JsonValue;
+  validationErrors?: any[];
+  hidden?: boolean;
+  renderTrigger?: boolean;
 };
 
-const defaultProps = {
-  renderTrigger: false,
-  hidden: false,
-  validationErrors: [],
-};
+export default class Control extends React.PureComponent<
+  ControlProps,
+  { hovered: boolean }
+> {
+  onMouseEnter: () => void;
+
+  onMouseLeave: () => void;
 
-export default class Control extends React.PureComponent {
-  constructor(props) {
+  constructor(props: ControlProps) {
     super(props);
     this.state = { hovered: false };
     this.onChange = this.onChange.bind(this);
@@ -69,11 +58,11 @@ export default class Control extends React.PureComponent {
     this.onMouseLeave = this.setHover.bind(this, false);
   }
 
-  onChange(value, errors) {
+  onChange(value: any, errors: any[]) {
     this.props.actions.setControlValue(this.props.name, value, errors);
   }
 
-  setHover(hovered) {
+  setHover(hovered: boolean) {
     this.setState({ hovered });
   }
 
@@ -98,6 +87,3 @@ export default class Control extends React.PureComponent {
     );
   }
 }
-
-Control.propTypes = propTypes;
-Control.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx
index e79e71a..e834c6d 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx
@@ -28,7 +28,7 @@ import ControlPanelSection from './ControlPanelSection';
 import ControlRow from './ControlRow';
 import Control from './Control';
 import { sectionsToRender } from '../controlUtils';
-import * as exploreActions from '../actions/exploreActions';
+import { exploreActions } from '../actions/exploreActions';
 
 const propTypes = {
   actions: PropTypes.object.isRequired,
@@ -106,8 +106,8 @@ class ControlPanelsContainer extends React.Component {
 
     return (
       <Control
-        name={name}
         key={`control-${name}`}
+        name={name}
         validationErrors={validationErrors}
         actions={actions}
         formData={provideFormDataToProps ? formData : null}
diff --git a/superset-frontend/src/explore/components/PropertiesModal.tsx b/superset-frontend/src/explore/components/PropertiesModal.tsx
index 3c83e0b..12e006d 100644
--- a/superset-frontend/src/explore/components/PropertiesModal.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef, useCallback } from 'react';
 import {
   Modal,
   Row,
@@ -31,18 +31,10 @@ import { OptionsType } from 'react-select/src/types';
 import { AsyncSelect } from 'src/components/Select';
 import rison from 'rison';
 import { t, SupersetClient } from '@superset-ui/core';
-import Chart from 'src/types/Chart';
+import Chart, { Slice } from 'src/types/Chart';
 import FormLabel from 'src/components/FormLabel';
 import getClientErrorObject from '../../utils/getClientErrorObject';
 
-export type Slice = {
-  id?: number;
-  slice_id: number;
-  slice_name: string;
-  description: string | null;
-  cache_timeout: number | null;
-};
-
 type InternalProps = {
   slice: Slice;
   onHide: () => void;
@@ -81,28 +73,31 @@ function PropertiesModal({ slice, onHide, onSave }: InternalProps) {
     });
   }
 
-  async function fetchChartData() {
-    try {
-      const response = await SupersetClient.get({
-        endpoint: `/api/v1/chart/${slice.slice_id}`,
-      });
-      const chart = response.json.result;
-      setOwners(
-        chart.owners.map((owner: any) => ({
-          value: owner.id,
-          label: `${owner.first_name} ${owner.last_name}`,
-        })),
-      );
-    } catch (response) {
-      const clientError = await getClientErrorObject(response);
-      showError(clientError);
-    }
-  }
+  const fetchChartData = useCallback(
+    async function fetchChartData() {
+      try {
+        const response = await SupersetClient.get({
+          endpoint: `/api/v1/chart/${slice.slice_id}`,
+        });
+        const chart = response.json.result;
+        setOwners(
+          chart.owners.map((owner: any) => ({
+            value: owner.id,
+            label: `${owner.first_name} ${owner.last_name}`,
+          })),
+        );
+      } catch (response) {
+        const clientError = await getClientErrorObject(response);
+        showError(clientError);
+      }
+    },
+    [slice.slice_id],
+  );
 
   // get the owners of this slice
   useEffect(() => {
     fetchChartData();
-  }, []);
+  }, [fetchChartData]);
 
   const loadOptions = (input = '') => {
     const query = rison.encode({
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js b/superset-frontend/src/explore/reducers/exploreReducer.js
index 83cd62e..36d7908 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.js
+++ b/superset-frontend/src/explore/reducers/exploreReducer.js
@@ -72,19 +72,6 @@ export default function exploreReducer(state = {}, action) {
         isDatasourcesLoading: true,
       };
     },
-    [actions.FETCH_DATASOURCES_SUCCEEDED]() {
-      return {
-        ...state,
-        isDatasourcesLoading: false,
-      };
-    },
-    [actions.FETCH_DATASOURCES_FAILED]() {
-      return {
-        ...state,
-        isDatasourcesLoading: false,
-        controlPanelAlert: action.error,
-      };
-    },
     [actions.SET_DATASOURCES]() {
       return {
         ...state,
diff --git a/superset-frontend/src/messageToasts/actions/index.js b/superset-frontend/src/messageToasts/actions/index.ts
similarity index 64%
rename from superset-frontend/src/messageToasts/actions/index.js
rename to superset-frontend/src/messageToasts/actions/index.ts
index bfed047..4eeb2e1 100644
--- a/superset-frontend/src/messageToasts/actions/index.js
+++ b/superset-frontend/src/messageToasts/actions/index.ts
@@ -17,20 +17,18 @@
  * under the License.
  */
 import shortid from 'shortid';
+import { ToastType, ToastMeta } from '../types';
 
-import {
-  INFO_TOAST,
-  SUCCESS_TOAST,
-  WARNING_TOAST,
-  DANGER_TOAST,
-} from '../constants';
-
-export function getToastUuid(type) {
+export function getToastUuid(type: ToastType) {
   return `${type}-${shortid.generate()}`;
 }
 
 export const ADD_TOAST = 'ADD_TOAST';
-export function addToast({ toastType, text, duration = 8000 }) {
+export function addToast({
+  toastType,
+  text,
+  duration = 8000,
+}: Omit<ToastMeta, 'id'>) {
   return {
     type: ADD_TOAST,
     payload: {
@@ -43,7 +41,7 @@ export function addToast({ toastType, text, duration = 8000 }) {
 }
 
 export const REMOVE_TOAST = 'REMOVE_TOAST';
-export function removeToast(id) {
+export function removeToast(id: string) {
   return {
     type: REMOVE_TOAST,
     payload: {
@@ -54,25 +52,21 @@ export function removeToast(id) {
 
 // Different types of toasts
 export const ADD_INFO_TOAST = 'ADD_INFO_TOAST';
-export function addInfoToast(text) {
-  return dispatch =>
-    dispatch(addToast({ text, toastType: INFO_TOAST, duration: 4000 }));
+export function addInfoToast(text: string) {
+  return addToast({ text, toastType: ToastType.INFO, duration: 4000 });
 }
 
 export const ADD_SUCCESS_TOAST = 'ADD_SUCCESS_TOAST';
-export function addSuccessToast(text) {
-  return dispatch =>
-    dispatch(addToast({ text, toastType: SUCCESS_TOAST, duration: 4000 }));
+export function addSuccessToast(text: string) {
+  return addToast({ text, toastType: ToastType.SUCCESS, duration: 4000 });
 }
 
 export const ADD_WARNING_TOAST = 'ADD_WARNING_TOAST';
-export function addWarningToast(text) {
-  return dispatch =>
-    dispatch(addToast({ text, toastType: WARNING_TOAST, duration: 6000 }));
+export function addWarningToast(text: string) {
+  return addToast({ text, toastType: ToastType.WARNING, duration: 6000 });
 }
 
 export const ADD_DANGER_TOAST = 'ADD_DANGER_TOAST';
-export function addDangerToast(text) {
-  return dispatch =>
-    dispatch(addToast({ text, toastType: DANGER_TOAST, duration: 8000 }));
+export function addDangerToast(text: string) {
+  return addToast({ text, toastType: ToastType.DANGER, duration: 8000 });
 }
diff --git a/superset-frontend/src/messageToasts/components/Toast.tsx b/superset-frontend/src/messageToasts/components/Toast.tsx
index 5b06fa6..f9b9bf7 100644
--- a/superset-frontend/src/messageToasts/components/Toast.tsx
+++ b/superset-frontend/src/messageToasts/components/Toast.tsx
@@ -20,11 +20,10 @@ import { Alert } from 'react-bootstrap';
 import { styled } from '@superset-ui/core';
 import cx from 'classnames';
 import Interweave from 'interweave';
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
 import Icon from 'src/components/Icon';
-import { ToastType } from 'src/messageToasts/types';
-
-import { SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../constants';
+import { ToastType } from 'src/messageToasts/constants';
+import { ToastMeta } from '../types';
 
 const ToastContianer = styled.div`
   display: flex;
@@ -37,19 +36,21 @@ const ToastContianer = styled.div`
 `;
 
 interface ToastPresenterProps {
-  toast: { id: string; toastType: ToastType; text: string; duration: number };
+  toast: ToastMeta;
   onCloseToast: (id: string) => void;
 }
 
 export default function Toast({ toast, onCloseToast }: ToastPresenterProps) {
-  let hideTimer: ReturnType<typeof setTimeout>;
+  const hideTimer = useRef<ReturnType<typeof setTimeout>>();
   const [visible, setVisible] = useState(false);
   const showToast = () => {
     setVisible(true);
   };
 
-  const handleClosePress = () => {
-    clearTimeout(hideTimer);
+  const handleClosePress = useCallback(() => {
+    if (hideTimer.current) {
+      clearTimeout(hideTimer.current);
+    }
     // Wait for the transition
     setVisible(() => {
       setTimeout(() => {
@@ -57,18 +58,20 @@ export default function Toast({ toast, onCloseToast }: ToastPresenterProps) {
       }, 150);
       return false;
     });
-  };
+  }, [onCloseToast, toast.id]);
 
   useEffect(() => {
     setTimeout(showToast);
 
     if (toast.duration > 0) {
-      hideTimer = setTimeout(handleClosePress, toast.duration);
+      hideTimer.current = setTimeout(handleClosePress, toast.duration);
     }
     return () => {
-      clearTimeout(hideTimer);
+      if (hideTimer.current) {
+        clearTimeout(hideTimer.current);
+      }
     };
-  }, []);
+  }, [handleClosePress, toast.duration]);
 
   return (
     <Alert
@@ -77,17 +80,17 @@ export default function Toast({ toast, onCloseToast }: ToastPresenterProps) {
         'alert',
         'toast',
         visible && 'toast--visible',
-        toast.toastType === SUCCESS_TOAST && 'toast--success',
-        toast.toastType === WARNING_TOAST && 'toast--warning',
-        toast.toastType === DANGER_TOAST && 'toast--danger',
+        toast.toastType === ToastType.SUCCESS && 'toast--success',
+        toast.toastType === ToastType.WARNING && 'toast--warning',
+        toast.toastType === ToastType.DANGER && 'toast--danger',
       )}
     >
       <ToastContianer>
-        {toast.toastType === SUCCESS_TOAST && (
+        {toast.toastType === ToastType.SUCCESS && (
           <Icon name="circle-check-solid" />
         )}
-        {toast.toastType === WARNING_TOAST ||
-          (toast.toastType === DANGER_TOAST && <Icon name="error-solid" />)}
+        {toast.toastType === ToastType.WARNING ||
+          (toast.toastType === ToastType.DANGER && <Icon name="error-solid" />)}
         <Interweave content={toast.text} />
       </ToastContianer>
     </Alert>
diff --git a/superset-frontend/src/messageToasts/components/ToastPresenter.tsx b/superset-frontend/src/messageToasts/components/ToastPresenter.tsx
index 4ce4898..aba922c 100644
--- a/superset-frontend/src/messageToasts/components/ToastPresenter.tsx
+++ b/superset-frontend/src/messageToasts/components/ToastPresenter.tsx
@@ -18,7 +18,7 @@
  */
 import React from 'react';
 import { styled } from '@superset-ui/core';
-import { ToastType } from 'src/messageToasts/types';
+import { ToastMeta } from 'src/messageToasts/types';
 import Toast from './Toast';
 
 const StyledToastPresenter = styled.div`
@@ -67,25 +67,22 @@ const StyledToastPresenter = styled.div`
   }
 `;
 
-type ToastShape = {
-  id: string;
-  toastType: ToastType;
-  text: string;
-  duration: number;
-};
-
-interface ToastPresenterProps {
-  toasts: Array<ToastShape>;
+type ToastPresenterProps = {
+  toasts: Array<ToastMeta>;
   removeToast: () => void;
-}
+};
 
-const ToastPresenter = ({ toasts, removeToast }: ToastPresenterProps) =>
-  toasts.length > 0 && (
-    <StyledToastPresenter id="toast-presenter">
-      {toasts.map(toast => (
-        <Toast key={toast.id} toast={toast} onCloseToast={removeToast} />
-      ))}
-    </StyledToastPresenter>
+export default function ToastPresenter({
+  toasts,
+  removeToast,
+}: ToastPresenterProps) {
+  return (
+    toasts.length > 0 && (
+      <StyledToastPresenter id="toast-presenter">
+        {toasts.map(toast => (
+          <Toast key={toast.id} toast={toast} onCloseToast={removeToast} />
+        ))}
+      </StyledToastPresenter>
+    )
   );
-
-export default ToastPresenter;
+}
diff --git a/superset-frontend/src/messageToasts/constants.js b/superset-frontend/src/messageToasts/constants.ts
similarity index 74%
rename from superset-frontend/src/messageToasts/constants.js
rename to superset-frontend/src/messageToasts/constants.ts
index 91f41c4..e22616c 100644
--- a/superset-frontend/src/messageToasts/constants.js
+++ b/superset-frontend/src/messageToasts/constants.ts
@@ -17,7 +17,12 @@
  * under the License.
  */
 // Toast types
-export const INFO_TOAST = 'INFO_TOAST';
-export const SUCCESS_TOAST = 'SUCCESS_TOAST';
-export const WARNING_TOAST = 'WARNING_TOAST';
-export const DANGER_TOAST = 'DANGER_TOAST';
+import { ToastType } from './types';
+
+export { ToastType } from './types';
+
+// for backward compatibility
+export const INFO_TOAST = ToastType.INFO;
+export const SUCCES_TOAST = ToastType.SUCCESS;
+export const WARNING_TOAST = ToastType.WARNING;
+export const DANGER_TOAST = ToastType.DANGER;
diff --git a/superset-frontend/src/messageToasts/propShapes.js b/superset-frontend/src/messageToasts/propShapes.js
deleted file mode 100644
index 0ce2aea..0000000
--- a/superset-frontend/src/messageToasts/propShapes.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import PropTypes from 'prop-types';
-
-import {
-  INFO_TOAST,
-  SUCCESS_TOAST,
-  WARNING_TOAST,
-  DANGER_TOAST,
-} from './constants';
-
-// eslint-disable-next-line import/prefer-default-export
-export const toastShape = PropTypes.shape({
-  id: PropTypes.string.isRequired,
-  toastType: PropTypes.oneOf([
-    INFO_TOAST,
-    SUCCESS_TOAST,
-    WARNING_TOAST,
-    DANGER_TOAST,
-  ]).isRequired,
-  text: PropTypes.string.isRequired,
-  duration: PropTypes.number,
-});
diff --git a/superset-frontend/src/messageToasts/types.ts b/superset-frontend/src/messageToasts/types.ts
index 471a3a0..0a72ec3 100644
--- a/superset-frontend/src/messageToasts/types.ts
+++ b/superset-frontend/src/messageToasts/types.ts
@@ -16,9 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+export enum ToastType {
+  INFO = 'INFO_TOAST',
+  SUCCESS = 'SUCCESS_TOAST',
+  WARNING = 'WARNING_TOAST',
+  DANGER = 'DANGER_TOAST',
+}
 
-export type ToastType =
-  | 'INFO_TOAST'
-  | 'SUCCESS_TOAST'
-  | 'WARNING_TOAST'
-  | 'DANGER_TOAST';
+export interface ToastMeta {
+  id: string;
+  toastType: ToastType;
+  text: string;
+  duration: number;
+}
diff --git a/superset-frontend/src/messageToasts/utils/getToastsFromPyFlashMessages.js b/superset-frontend/src/messageToasts/utils/getToastsFromPyFlashMessages.js
index 26fe7b7..6f921c4 100644
--- a/superset-frontend/src/messageToasts/utils/getToastsFromPyFlashMessages.js
+++ b/superset-frontend/src/messageToasts/utils/getToastsFromPyFlashMessages.js
@@ -17,7 +17,7 @@
  * under the License.
  */
 import { addToast } from '../actions';
-import { INFO_TOAST, SUCCESS_TOAST, DANGER_TOAST } from '../constants';
+import { ToastType } from '../constants';
 
 export default function toastsFromPyFlashMessages(flashMessages = []) {
   const toasts = [];
@@ -25,8 +25,8 @@ export default function toastsFromPyFlashMessages(flashMessages = []) {
   flashMessages.forEach(([messageType, message]) => {
     const toastType =
       messageType === 'danger'
-        ? DANGER_TOAST
-        : (messageType === 'success' && SUCCESS_TOAST) || INFO_TOAST;
+        ? ToastType.DANGER
+        : (messageType === 'success' && ToastType.SUCCESS) || ToastType.INFO;
 
     const toast = addToast({
       text: message,
diff --git a/superset-frontend/src/types/Chart.ts b/superset-frontend/src/types/Chart.ts
index e78d810..5148d32 100644
--- a/superset-frontend/src/types/Chart.ts
+++ b/superset-frontend/src/types/Chart.ts
@@ -37,3 +37,11 @@ export default interface Chart {
   owners?: Owner[];
   datasource_name_text?: string;
 }
+
+export type Slice = {
+  id?: number;
+  slice_id: number;
+  slice_name: string;
+  description: string | null;
+  cache_timeout: number | null;
+};
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 4323f46..a22c54d 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -34,8 +34,8 @@ import ListView, {
   SelectOption,
 } from 'src/components/ListView';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
-import PropertiesModal, { Slice } from 'src/explore/components/PropertiesModal';
-import Chart from 'src/types/Chart';
+import PropertiesModal from 'src/explore/components/PropertiesModal';
+import Chart, { Slice } from 'src/types/Chart';
 import ListViewCard from 'src/components/ListViewCard';
 import Label from 'src/components/Label';
 import { Dropdown, Menu } from 'src/common/components';