You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/05/12 08:34:16 UTC

[incubator-superset] branch dashboard-builder updated: Markdown for dashboard (#4962)

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

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


The following commit(s) were added to refs/heads/dashboard-builder by this push:
     new 6b4e153  Markdown for dashboard (#4962)
6b4e153 is described below

commit 6b4e153526b66a5e0880439b7f0a7f1b9c6a253e
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Sat May 12 01:34:13 2018 -0700

    Markdown for dashboard (#4962)
---
 superset/assets/package.json                       |   3 +-
 .../assets/src/dashboard/actions/sliceEntities.js  |  46 +++--
 .../dashboard/components/BuilderComponentPane.jsx  |  18 +-
 .../src/dashboard/components/DashboardBuilder.jsx  |   2 +
 .../components/gridComponents/Markdown.jsx         | 225 +++++++++++++++++++++
 .../dashboard/components/gridComponents/index.js   |   4 +
 .../components/gridComponents/new/NewMarkdown.jsx  |  16 ++
 .../components/menu/MarkdownModeDropdown.jsx       |  39 ++++
 .../src/dashboard/containers/DashboardBuilder.jsx  |   2 +
 .../src/dashboard/reducers/getInitialState.js      |  38 ++--
 .../dashboard/stylesheets/builder-sidepane.less    |  12 +-
 .../dashboard/stylesheets/components/index.less    |   1 +
 .../dashboard/stylesheets/components/markdown.less |  11 +
 .../src/dashboard/util/dashboardLayoutConverter.js |  84 +++++---
 superset/assets/src/modules/utils.js               |   4 +
 superset/views/core.py                             |   2 +-
 16 files changed, 430 insertions(+), 77 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index 576920a..33fe5cc 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -85,7 +85,7 @@
     "prop-types": "^15.6.0",
     "re-resizable": "^4.3.1",
     "react": "^15.6.2",
-    "react-ace": "^5.0.1",
+    "react-ace": "^5.10.0",
     "react-addons-css-transition-group": "^15.6.0",
     "react-addons-shallow-compare": "^15.4.2",
     "react-alert": "^2.3.0",
@@ -98,6 +98,7 @@
     "react-dom": "^15.6.2",
     "react-gravatar": "^2.6.1",
     "react-map-gl": "^3.0.4",
+    "react-markdown": "^3.3.0",
     "react-redux": "^5.0.2",
     "react-resizable": "^1.3.3",
     "react-search-input": "^0.11.3",
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
index 37781f9..b635ea0 100644
--- a/superset/assets/src/dashboard/actions/sliceEntities.js
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -1,6 +1,8 @@
 /* eslint camelcase: 0 */
 import $ from 'jquery';
 
+import { getDatasourceParameter } from '../../modules/utils';
+
 export const SET_ALL_SLICES = 'SET_ALL_SLICES';
 export function setAllSlices(slices) {
   return { type: SET_ALL_SLICES, slices };
@@ -29,22 +31,34 @@ export function fetchAllSlices(userId) {
         success: response => {
           const slices = {};
           response.result.forEach(slice => {
-            const form_data = JSON.parse(slice.params);
-            slices[slice.id] = {
-              slice_id: slice.id,
-              slice_url: slice.slice_url,
-              slice_name: slice.slice_name,
-              edit_url: slice.edit_url,
-              form_data,
-              datasource: form_data.datasource,
-              datasource_name: slice.datasource_name_text,
-              datasource_link: slice.datasource_link,
-              changed_on: new Date(slice.changed_on).getTime(),
-              description: slice.description,
-              description_markdown: slice.description_markeddown,
-              viz_type: slice.viz_type,
-              modified: slice.modified,
-            };
+            let form_data = JSON.parse(slice.params);
+            let datasource = form_data.datasource;
+            if (!datasource) {
+              datasource = getDatasourceParameter(
+                slice.datasource_id,
+                slice.datasource_type,
+              );
+              form_data = {
+                ...form_data,
+                datasource,
+              };
+            }
+            if (['markup', 'separator'].indexOf(slice.viz_type) === -1) {
+              slices[slice.id] = {
+                slice_id: slice.id,
+                slice_url: slice.slice_url,
+                slice_name: slice.slice_name,
+                edit_url: slice.edit_url,
+                form_data,
+                datasource_name: slice.datasource_name_text,
+                datasource_link: slice.datasource_link,
+                changed_on: new Date(slice.changed_on).getTime(),
+                description: slice.description,
+                description_markdown: slice.description_markeddown,
+                viz_type: slice.viz_type,
+                modified: slice.modified,
+              };
+            }
           });
           return dispatch(setAllSlices(slices));
         },
diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index b42650e..c35a637 100644
--- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -9,11 +9,13 @@ import NewDivider from './gridComponents/new/NewDivider';
 import NewHeader from './gridComponents/new/NewHeader';
 import NewRow from './gridComponents/new/NewRow';
 import NewTabs from './gridComponents/new/NewTabs';
+import NewMarkdown from './gridComponents/new/NewMarkdown';
 import SliceAdder from '../containers/SliceAdder';
 import { t } from '../../locales';
 
 const propTypes = {
   topOffset: PropTypes.number,
+  toggleBuilderPane: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -52,7 +54,12 @@ class BuilderComponentPane extends React.PureComponent {
               >
                 <div className="component-layer slide-content">
                   <div className="dashboard-builder-sidepane-header">
-                    {t('Saved components')}
+                    <span>{t('Insert')}</span>
+                    <i
+                      className="fa fa-times trigger"
+                      onClick={this.props.toggleBuilderPane}
+                      role="none"
+                    />
                   </div>
                   <div
                     className="new-component static"
@@ -67,17 +74,12 @@ class BuilderComponentPane extends React.PureComponent {
                     <i className="fa fa-arrow-right trigger" />
                   </div>
 
-                  <div className="dashboard-builder-sidepane-header">
-                    {t('Containers')}
-                  </div>
                   <NewTabs />
                   <NewRow />
                   <NewColumn />
 
-                  <div className="dashboard-builder-sidepane-header">
-                    {t('More components')}
-                  </div>
                   <NewHeader />
+                  <NewMarkdown />
                   <NewDivider />
                 </div>
                 <div className="slices-layer slide-content">
@@ -87,7 +89,7 @@ class BuilderComponentPane extends React.PureComponent {
                     role="none"
                   >
                     <i className="fa fa-arrow-left trigger" />
-                    {t('All components')}
+                    <span>{t('All components')}</span>
                   </div>
                   <SliceAdder height={calculatedHeight} />
                 </div>
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 7f92948..0951ebf 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -34,6 +34,7 @@ const propTypes = {
   editMode: PropTypes.bool.isRequired,
   showBuilderPane: PropTypes.bool,
   handleComponentDrop: PropTypes.func.isRequired,
+  toggleBuilderPane: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -184,6 +185,7 @@ class DashboardBuilder extends React.Component {
             this.props.showBuilderPane && (
               <BuilderComponentPane
                 topOffset={topLevelTabs ? TABS_HEIGHT : 0}
+                toggleBuilderPane={this.props.toggleBuilderPane}
               />
             )}
         </div>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
new file mode 100644
index 0000000..459f89a
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
@@ -0,0 +1,225 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactMarkdown from 'react-markdown';
+import AceEditor from 'react-ace';
+import 'brace/mode/markdown';
+import 'brace/theme/textmate';
+
+import DeleteComponentButton from '../DeleteComponentButton';
+import DragDroppable from '../dnd/DragDroppable';
+import ResizableContainer from '../resizable/ResizableContainer';
+import MarkdownModeDropdown from '../menu/MarkdownModeDropdown';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+import { componentShape } from '../../util/propShapes';
+import { ROW_TYPE, COLUMN_TYPE } from '../../util/componentTypes';
+import {
+  GRID_MIN_COLUMN_COUNT,
+  GRID_MIN_ROW_UNITS,
+  GRID_BASE_UNIT,
+} from '../../util/constants';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
+
+  // grid related
+  availableColumnCount: PropTypes.number.isRequired,
+  columnWidth: PropTypes.number.isRequired,
+  onResizeStart: PropTypes.func.isRequired,
+  onResize: PropTypes.func.isRequired,
+  onResizeStop: PropTypes.func.isRequired,
+
+  // dnd
+  deleteComponent: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+};
+
+const defaultProps = {};
+const markdownPlaceHolder = `### New Markdown
+Insert *bold* or _italic_ text, and (urls)[www.url.com] here.`;
+
+class Markdown extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isFocused: false,
+      markdownSource: props.component.meta.code,
+      editor: null,
+      editorMode: props.component.meta.code ? 'preview' : 'edit', // show edit mode when code is empty
+    };
+
+    this.handleChangeFocus = this.handleChangeFocus.bind(this);
+    this.handleChangeEditorMode = this.handleChangeEditorMode.bind(this);
+    this.handleMarkdownChange = this.handleMarkdownChange.bind(this);
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+    this.setEditor = this.setEditor.bind(this);
+  }
+
+  componentDidUpdate(prevProps) {
+    if (
+      this.state.editor &&
+      (prevProps.component.meta.width !== this.props.component.meta.width ||
+        prevProps.columnWidth !== this.props.columnWidth)
+    ) {
+      this.state.editor.resize(true);
+    }
+  }
+
+  setEditor(editor) {
+    editor.getSession().setUseWrapMode(true);
+    this.setState({
+      editor,
+    });
+  }
+
+  handleChangeFocus(nextFocus) {
+    this.setState(() => ({ isFocused: Boolean(nextFocus) }));
+  }
+
+  handleChangeEditorMode(mode) {
+    if (this.state.editorMode === 'edit') {
+      const { updateComponents, component } = this.props;
+      if (component.meta.code !== this.state.markdownSource) {
+        updateComponents({
+          [component.id]: {
+            ...component,
+            meta: {
+              ...component.meta,
+              code: this.state.markdownSource,
+            },
+          },
+        });
+      }
+    }
+
+    this.setState(() => ({
+      editorMode: mode,
+    }));
+  }
+
+  handleMarkdownChange(nextValue) {
+    this.setState({
+      markdownSource: nextValue,
+    });
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  renderEditMode() {
+    return (
+      <AceEditor
+        mode="markdown"
+        theme="textmate"
+        onChange={this.handleMarkdownChange}
+        width={'100%'}
+        height={'100%'}
+        editorProps={{ $blockScrolling: true }}
+        value={this.state.markdownSource || markdownPlaceHolder}
+        readOnly={false}
+        onLoad={this.setEditor}
+      />
+    );
+  }
+
+  renderPreviewMode() {
+    return (
+      <ReactMarkdown source={this.state.markdownSource} escapeHtml={false} />
+    );
+  }
+
+  render() {
+    const { isFocused } = this.state;
+
+    const {
+      component,
+      parentComponent,
+      index,
+      depth,
+      availableColumnCount,
+      columnWidth,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+      handleComponentDrop,
+      editMode,
+    } = this.props;
+
+    // inherit the size of parent columns
+    const widthMultiple =
+      parentComponent.type === COLUMN_TYPE
+        ? parentComponent.meta.width || GRID_MIN_COLUMN_COUNT
+        : component.meta.width || GRID_MIN_COLUMN_COUNT;
+
+    return (
+      <DragDroppable
+        component={component}
+        parentComponent={parentComponent}
+        orientation={depth % 2 === 1 ? 'column' : 'row'}
+        index={index}
+        depth={depth}
+        onDrop={handleComponentDrop}
+        disableDragDrop={isFocused}
+        editMode={editMode}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <WithPopoverMenu
+            onChangeFocus={this.handleChangeFocus}
+            menuItems={[
+              <MarkdownModeDropdown
+                id={`${component.id}-mode`}
+                value={this.state.editorMode}
+                onChange={this.handleChangeEditorMode}
+              />,
+              <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
+            ]}
+            editMode={editMode}
+          >
+            <div className="dashboard-markdown">
+              <ResizableContainer
+                id={component.id}
+                adjustableWidth={parentComponent.type === ROW_TYPE}
+                adjustableHeight
+                widthStep={columnWidth}
+                widthMultiple={widthMultiple}
+                heightStep={GRID_BASE_UNIT}
+                heightMultiple={component.meta.height}
+                minWidthMultiple={GRID_MIN_COLUMN_COUNT}
+                minHeightMultiple={GRID_MIN_ROW_UNITS}
+                maxWidthMultiple={availableColumnCount + widthMultiple}
+                onResizeStart={onResizeStart}
+                onResize={onResize}
+                onResizeStop={onResizeStop}
+                editMode={editMode}
+              >
+                <div
+                  ref={dragSourceRef}
+                  className="dashboard-component dashboard-component-chart-holder"
+                >
+                  {editMode && this.state.editorMode === 'edit'
+                    ? this.renderEditMode()
+                    : this.renderPreviewMode()}
+                </div>
+
+                {dropIndicatorProps && <div {...dropIndicatorProps} />}
+              </ResizableContainer>
+            </div>
+          </WithPopoverMenu>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+Markdown.propTypes = propTypes;
+Markdown.defaultProps = defaultProps;
+
+export default Markdown;
diff --git a/superset/assets/src/dashboard/components/gridComponents/index.js b/superset/assets/src/dashboard/components/gridComponents/index.js
index 016ab03..c56bed0 100644
--- a/superset/assets/src/dashboard/components/gridComponents/index.js
+++ b/superset/assets/src/dashboard/components/gridComponents/index.js
@@ -1,5 +1,6 @@
 import {
   CHART_TYPE,
+  MARKDOWN_TYPE,
   COLUMN_TYPE,
   DIVIDER_TYPE,
   HEADER_TYPE,
@@ -9,6 +10,7 @@ import {
 } from '../../util/componentTypes';
 
 import ChartHolder from './ChartHolder';
+import Markdown from './Markdown';
 import Column from './Column';
 import Divider from './Divider';
 import Header from './Header';
@@ -17,6 +19,7 @@ import Tab from './Tab';
 import Tabs from './Tabs';
 
 export { default as ChartHolder } from './ChartHolder';
+export { default as Markdown } from './Markdown';
 export { default as Column } from './Column';
 export { default as Divider } from './Divider';
 export { default as Header } from './Header';
@@ -26,6 +29,7 @@ export { default as Tabs } from './Tabs';
 
 export default {
   [CHART_TYPE]: ChartHolder,
+  [MARKDOWN_TYPE]: Markdown,
   [COLUMN_TYPE]: Column,
   [DIVIDER_TYPE]: Divider,
   [HEADER_TYPE]: Header,
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewMarkdown.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewMarkdown.jsx
new file mode 100644
index 0000000..e4c8892
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewMarkdown.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { MARKDOWN_TYPE } from '../../../util/componentTypes';
+import { NEW_MARKDOWN_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewDivider() {
+  return (
+    <DraggableNewComponent
+      id={NEW_MARKDOWN_ID}
+      type={MARKDOWN_TYPE}
+      label="Markdown"
+      className="fa fa-code"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/menu/MarkdownModeDropdown.jsx b/superset/assets/src/dashboard/components/menu/MarkdownModeDropdown.jsx
new file mode 100644
index 0000000..10aa932
--- /dev/null
+++ b/superset/assets/src/dashboard/components/menu/MarkdownModeDropdown.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { t } from '../../../locales';
+
+import PopoverDropdown from './PopoverDropdown';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  value: PropTypes.string.isRequired,
+  onChange: PropTypes.func.isRequired,
+};
+
+const dropdownOptions = [
+  {
+    value: 'edit',
+    label: t('Edit'),
+  },
+  {
+    value: 'preview',
+    label: t('Preview'),
+  },
+];
+
+export default class MarkdownModeDropdown extends React.PureComponent {
+  render() {
+    const { id, value, onChange } = this.props;
+
+    return (
+      <PopoverDropdown
+        id={id}
+        options={dropdownOptions}
+        value={value}
+        onChange={onChange}
+      />
+    );
+  }
+}
+
+MarkdownModeDropdown.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
index 6bece3d..fde1e76 100644
--- a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
@@ -2,6 +2,7 @@ import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 import DashboardBuilder from '../components/DashboardBuilder';
 
+import { toggleBuilderPane } from '../actions/dashboardState';
 import {
   deleteTopLevelTabs,
   handleComponentDrop,
@@ -20,6 +21,7 @@ function mapDispatchToProps(dispatch) {
     {
       deleteTopLevelTabs,
       handleComponentDrop,
+      toggleBuilderPane,
     },
     dispatch,
   );
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index ba24b36..b209043 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -74,26 +74,28 @@ export default function(bootstrapData) {
   const sliceIds = new Set();
   dashboard.slices.forEach(slice => {
     const key = slice.slice_id;
-    chartQueries[key] = {
-      ...chart,
-      id: key,
-      form_data: slice.form_data,
-      formData: applyDefaultFormData(slice.form_data),
-    };
+    if (['separator', 'markup'].indexOf(slice.form_data.viz_type) === -1) {
+      chartQueries[key] = {
+        ...chart,
+        id: key,
+        form_data: slice.form_data,
+        formData: applyDefaultFormData(slice.form_data),
+      };
 
-    slices[key] = {
-      slice_id: key,
-      slice_url: slice.slice_url,
-      slice_name: slice.slice_name,
-      form_data: slice.form_data,
-      edit_url: slice.edit_url,
-      viz_type: slice.form_data.viz_type,
-      datasource: slice.form_data.datasource,
-      description: slice.description,
-      description_markeddown: slice.description_markeddown,
-    };
+      slices[key] = {
+        slice_id: key,
+        slice_url: slice.slice_url,
+        slice_name: slice.slice_name,
+        form_data: slice.form_data,
+        edit_url: slice.edit_url,
+        viz_type: slice.form_data.viz_type,
+        datasource: slice.form_data.datasource,
+        description: slice.description,
+        description_markeddown: slice.description_markeddown,
+      };
 
-    sliceIds.add(key);
+      sliceIds.add(key);
+    }
 
     // sync layout names with current slice names in case a slice was edited
     // in explore since the layout was updated. name updates go through layout for undo/redo
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index d45da4f..5f87d0c 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -10,13 +10,21 @@
     border-top: 1px solid @gray-light;
     border-bottom: 1px solid @gray-light;
     padding: 16px;
+    display: flex;
+    align-items: center;
   }
 
   .trigger {
-    height: 18px;
-    width: 25px;
+    font-size: 16px;
     color: @almost-black;
     opacity: 1;
+    margin-left: auto;
+    cursor: pointer;
+  }
+
+  .slices-layer .trigger {
+    margin-left: 0;
+    margin-right: 20px;
   }
 
   .viewport {
diff --git a/superset/assets/src/dashboard/stylesheets/components/index.less b/superset/assets/src/dashboard/stylesheets/components/index.less
index 5a1803e..5f8d610 100644
--- a/superset/assets/src/dashboard/stylesheets/components/index.less
+++ b/superset/assets/src/dashboard/stylesheets/components/index.less
@@ -5,3 +5,4 @@
 @import './new-component.less';
 @import './row.less';
 @import './tabs.less';
+@import './markdown.less';
\ No newline at end of file
diff --git a/superset/assets/src/dashboard/stylesheets/components/markdown.less b/superset/assets/src/dashboard/stylesheets/components/markdown.less
new file mode 100644
index 0000000..d377c68
--- /dev/null
+++ b/superset/assets/src/dashboard/stylesheets/components/markdown.less
@@ -0,0 +1,11 @@
+.dashboard-markdown {
+  overflow: hidden;
+
+  .dashboard--editing & {
+    cursor: move;
+  }
+
+  #brace-editor {
+    border: none;
+  }
+}
\ No newline at end of file
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index f3f6061..e28e3be 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -5,6 +5,7 @@ import {
   ROW_TYPE,
   COLUMN_TYPE,
   CHART_TYPE,
+  MARKDOWN_TYPE,
   DASHBOARD_ROOT_TYPE,
   DASHBOARD_GRID_TYPE,
 } from './componentTypes';
@@ -76,11 +77,22 @@ function getColContainer() {
 }
 
 function getChartHolder(item) {
-  const { size_x, size_y, slice_id } = item;
+  const { size_x, size_y, slice_id, code } = item;
 
   const width = Math.max(1, Math.floor(size_x / GRID_RATIO));
   const height = Math.max(1, Math.round(size_y / GRID_RATIO));
-
+  if (code !== undefined) {
+    return {
+      type: MARKDOWN_TYPE,
+      id: `DASHBOARD_MARKDOWN_TYPE-${generateId()}`,
+      children: [],
+      meta: {
+        width,
+        height: Math.round(height * 100 / ROW_HEIGHT),
+        code,
+      },
+    };
+  }
   return {
     type: CHART_TYPE,
     id: `DASHBOARD_CHART_TYPE-${generateId()}`,
@@ -135,7 +147,7 @@ function doConvert(positions, level, parent, root) {
 
   if (positions.length === 1 || level >= MAX_RECURSIVE_LEVEL) {
     // special treatment for single chart dash, always wrap chart inside a row
-    if (parent.type === 'DASHBOARD_GRID_TYPE') {
+    if (parent.type === DASHBOARD_GRID_TYPE) {
       const rowContainer = getRowContainer();
       root[rowContainer.id] = rowContainer;
       parent.children.push(rowContainer.id);
@@ -181,7 +193,7 @@ function doConvert(positions, level, parent, root) {
       return;
     }
 
-    if (layer.length === 1) {
+    if (layer.length === 1 && parent.type === COLUMN_TYPE) {
       const chartHolder = getChartHolder(layer[0]);
       root[chartHolder.id] = chartHolder;
       parent.children.push(chartHolder.id);
@@ -262,6 +274,35 @@ function doConvert(positions, level, parent, root) {
   });
 }
 
+export function convertToLayout(positions) {
+  const root = {
+    [DASHBOARD_VERSION_KEY]: 'v2',
+    [DASHBOARD_ROOT_ID]: {
+      type: DASHBOARD_ROOT_TYPE,
+      id: DASHBOARD_ROOT_ID,
+      children: [DASHBOARD_GRID_ID],
+    },
+    [DASHBOARD_GRID_ID]: {
+      type: DASHBOARD_GRID_TYPE,
+      id: DASHBOARD_GRID_ID,
+      children: [],
+    },
+  };
+
+  doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
+
+  // remove row's width/height and col's height
+  Object.values(root).forEach(item => {
+    if (ROW_TYPE === item.type) {
+      const meta = item.meta;
+      delete meta.width;
+    }
+  });
+
+  // console.log(JSON.stringify(root));
+  return root;
+}
+
 export default function(dashboard) {
   const positions = [];
 
@@ -281,7 +322,7 @@ export default function(dashboard) {
     Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
   );
   let newSliceCounter = 0;
-  dashboard.slices.forEach(({ slice_id }) => {
+  dashboard.slices.forEach(({ slice_id, form_data }) => {
     let position = positionDict[slice_id];
     if (!position) {
       // append new slices to dashboard bottom, 3 slices per row
@@ -294,33 +335,14 @@ export default function(dashboard) {
       };
       newSliceCounter += 1;
     }
-
-    positions.push(position);
-  });
-
-  const root = {
-    [DASHBOARD_VERSION_KEY]: 'v2',
-    [DASHBOARD_ROOT_ID]: {
-      type: DASHBOARD_ROOT_TYPE,
-      id: DASHBOARD_ROOT_ID,
-      children: [DASHBOARD_GRID_ID],
-    },
-    [DASHBOARD_GRID_ID]: {
-      type: DASHBOARD_GRID_TYPE,
-      id: DASHBOARD_GRID_ID,
-      children: [],
-    },
-  };
-
-  doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
-
-  // remove row's width/height and col's height
-  Object.values(root).forEach(item => {
-    if (ROW_TYPE === item.type) {
-      const meta = item.meta;
-      delete meta.width;
+    if (form_data && ['markup', 'separator'].indexOf(form_data.viz_type) > -1) {
+      position = {
+        ...position,
+        code: form_data.code,
+      };
     }
+    positions.push(position);
   });
 
-  return root;
+  return convertToLayout(positions);
 }
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index c4ea9ce..eb937bb 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -202,6 +202,10 @@ export function getAjaxErrorMsg(error) {
           error.responseText;
 }
 
+export function getDatasourceParameter(datasourceId, datasourceType) {
+  return `${datasourceId}__${datasourceType}`;
+}
+
 export function customizeToolTip(chart, xAxisFormatter, yAxisFormatters) {
   chart.useInteractiveGuideline(true);
   chart.interactiveLayer.tooltip.contentGenerator(function (d) {
diff --git a/superset/views/core.py b/superset/views/core.py
index fde5be7..2ff3500 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -518,7 +518,7 @@ appbuilder.add_view_no_menu(SliceAsync)
 class SliceAddView(SliceModelView):  # noqa
     list_columns = [
         'id', 'slice_name', 'slice_url', 'edit_url', 'viz_type', 'params',
-        'description', 'description_markeddown',
+        'description', 'description_markeddown', 'datasource_id', 'datasource_type',
         'datasource_name_text', 'datasource_link',
         'owners', 'modified', 'changed_on']
 

-- 
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.