superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From grace...@apache.org
Subject [incubator-superset] branch master updated: feat(dashboard): direct link to single chart/tab/header in dashboard (#6964)
Date Tue, 09 Apr 2019 22:42:53 GMT
This is an automated email from the ASF dual-hosted git repository.

graceguo 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 c50e6bc  feat(dashboard): direct link to single chart/tab/header in dashboard (#6964)
c50e6bc is described below

commit c50e6bc98155e1a13b79c71d9ed47767718cce92
Author: Grace Guo <grace.guo@airbnb.com>
AuthorDate: Tue Apr 9 15:42:46 2019 -0700

    feat(dashboard): direct link to single chart/tab/header in dashboard (#6964)
    
    * direct display for pre-selected tab
    
    * update parents
    
    * add AnchorLink component
    
    * add unit tests
---
 .../javascripts/components/AnchorLink_spec.jsx     |  63 +++++++++++++
 .../dashboard/actions/dashboardState_spec.js       | 100 +++++++++++++++++++++
 .../components/gridComponents/Header_spec.jsx      |  11 ++-
 .../components/gridComponents/Tabs_spec.jsx        |  18 +++-
 .../dashboard/fixtures/mockDashboardData.js}       |  14 +--
 .../dashboard/fixtures/mockDashboardLayout.js      |  27 +++++-
 .../util/findTabIndexByComponentId_spec.js         |  85 ++++++++++++++++++
 .../util/updateComponentParentsList_spec.js        |  97 ++++++++++++++++++++
 superset/assets/src/components/AnchorLink.jsx      |  99 ++++++++++++++++++++
 .../assets/src/components/URLShortLinkButton.jsx   |   4 +-
 .../assets/src/components/URLShortLinkModal.jsx    |   3 +-
 .../src/dashboard/actions/dashboardLayout.js       |   3 +
 .../assets/src/dashboard/actions/dashboardState.js |  21 ++---
 .../src/dashboard/components/DashboardBuilder.jsx  |  16 +++-
 .../dashboard/components/HeaderActionsDropdown.jsx |   6 +-
 .../src/dashboard/components/SliceHeader.jsx       |   9 ++
 .../dashboard/components/SliceHeaderControls.jsx   |  27 +++++-
 .../dashboard/components/gridComponents/Chart.jsx  |   7 ++
 .../components/gridComponents/ChartHolder.jsx      |   3 +
 .../dashboard/components/gridComponents/Header.jsx |  10 +++
 .../dashboard/components/gridComponents/Tab.jsx    |  19 +++-
 .../dashboard/components/gridComponents/Tabs.jsx   |  10 ++-
 superset/assets/src/dashboard/containers/Chart.jsx |   4 +-
 .../src/dashboard/containers/DashboardBuilder.jsx  |   1 +
 .../dashboard/containers/DashboardComponent.jsx    |   8 ++
 .../src/dashboard/fixtures/emptyDashboardLayout.js |   1 +
 .../src/dashboard/reducers/dashboardLayout.js      |  17 ++++
 .../src/dashboard/reducers/getInitialState.js      |  15 +++-
 ...EmptyLayout.js => findTabIndexByComponentId.js} |  42 ++++-----
 .../assets/src/dashboard/util/getDashboardUrl.js   |   5 +-
 .../assets/src/dashboard/util/getEmptyLayout.js    |   1 +
 ...mptyLayout.js => updateComponentParentsList.js} |  36 ++++----
 superset/assets/stylesheets/superset.less          |  45 ++++++++++
 33 files changed, 754 insertions(+), 73 deletions(-)

diff --git a/superset/assets/spec/javascripts/components/AnchorLink_spec.jsx b/superset/assets/spec/javascripts/components/AnchorLink_spec.jsx
new file mode 100644
index 0000000..4281109
--- /dev/null
+++ b/superset/assets/spec/javascripts/components/AnchorLink_spec.jsx
@@ -0,0 +1,63 @@
+/**
+ * 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 React from 'react';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+
+import AnchorLink from '../../../src/components/AnchorLink';
+import URLShortLinkButton from '../../../src/components/URLShortLinkButton';
+
+describe('AnchorLink', () => {
+  const props = {
+    anchorLinkId: 'CHART-123',
+  };
+
+  it('should scroll the AnchorLink into view upon mount', () => {
+    const callback = sinon.spy();
+    const clock = sinon.useFakeTimers();
+    const stub = sinon.stub(document, 'getElementById').returns({
+      scrollIntoView: callback,
+    });
+
+    const wrapper = shallow(<AnchorLink {...props} />);
+    wrapper.instance().getLocationHash = () => (props.anchorLinkId);
+    wrapper.update();
+
+    wrapper.instance().componentDidMount();
+    clock.tick(2000);
+    expect(callback.callCount).toEqual(1);
+    stub.restore();
+  });
+
+  it('should render anchor link with id', () => {
+    const wrapper = shallow(<AnchorLink {...props} />);
+    expect(wrapper.find(`#${props.anchorLinkId}`)).toHaveLength(1);
+    expect(wrapper.find(URLShortLinkButton)).toHaveLength(0);
+  });
+
+  it('should render URLShortLinkButton', () => {
+    const wrapper = shallow(<AnchorLink {...props} showShortLinkButton />);
+    expect(wrapper.find(URLShortLinkButton)).toHaveLength(1);
+    expect(wrapper.find(URLShortLinkButton).prop('placement')).toBe('right');
+
+    const targetUrl = wrapper.find(URLShortLinkButton).prop('url');
+    const hash = targetUrl.slice(targetUrl.indexOf('#') + 1);
+    expect(hash).toBe(props.anchorLinkId);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardState_spec.js
new file mode 100644
index 0000000..8ba499e
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardState_spec.js
@@ -0,0 +1,100 @@
+/**
+ * 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 sinon from 'sinon';
+import { SupersetClient } from '@superset-ui/connection';
+
+import { saveDashboardRequest } from '../../../../src/dashboard/actions/dashboardState';
+import { UPDATE_COMPONENTS_PARENTS_LIST } from '../../../../src/dashboard/actions/dashboardLayout';
+import mockDashboardData from '../fixtures/mockDashboardData';
+import { DASHBOARD_GRID_ID } from '../../../../src/dashboard/util/constants';
+
+describe('dashboardState actions', () => {
+  const mockState = {
+    dashboardState: {
+      hasUnsavedChanges: true,
+    },
+    dashboardInfo: {},
+    dashboardLayout: {
+      past: [],
+      present: mockDashboardData.positions,
+      future: {},
+    },
+  };
+  const newDashboardData = mockDashboardData;
+
+  let postStub;
+  beforeEach(() => {
+    postStub = sinon
+      .stub(SupersetClient, 'post')
+      .resolves('the value you want to return');
+  });
+  afterEach(() => {
+    postStub.restore();
+  });
+
+  function setup(stateOverrides) {
+    const state = { ...mockState, ...stateOverrides };
+    const getState = sinon.spy(() => state);
+    const dispatch = sinon.stub();
+    return { getState, dispatch, state };
+  }
+
+  describe('saveDashboardRequest', () => {
+    it('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: false },
+      });
+      const thunk = saveDashboardRequest(newDashboardData, 1, 'save_dash');
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).toBe(1);
+      expect(dispatch.getCall(0).args[0].type).toBe(
+        UPDATE_COMPONENTS_PARENTS_LIST,
+      );
+    });
+
+    it('should post dashboard data with updated redux state', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: false },
+      });
+
+      // start with mockDashboardData, it didn't have parents attr
+      expect(
+        newDashboardData.positions[DASHBOARD_GRID_ID].parents,
+      ).not.toBeDefined();
+
+      // mock redux work: dispatch an event, cause modify redux state
+      const mockParentsList = ['ROOT_ID'];
+      dispatch.callsFake(() => {
+        mockState.dashboardLayout.present[
+          DASHBOARD_GRID_ID
+        ].parents = mockParentsList;
+      });
+
+      // call saveDashboardRequest, it should post dashboard data with updated
+      // layout object (with parents attribute)
+      const thunk = saveDashboardRequest(newDashboardData, 1, 'save_dash');
+      thunk(dispatch, getState);
+      expect(postStub.callCount).toBe(1);
+      const postPayload = postStub.getCall(0).args[0].postPayload;
+      expect(postPayload.data.positions[DASHBOARD_GRID_ID].parents).toBe(
+        mockParentsList,
+      );
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Header_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Header_spec.jsx
index b206184..30121f2 100644
--- a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Header_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Header_spec.jsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import React from 'react';
+import { Provider } from 'react-redux';
 import { mount } from 'enzyme';
 import sinon from 'sinon';
 
@@ -33,6 +34,7 @@ import {
 } from '../../../../../src/dashboard/util/componentTypes';
 
 import WithDragDropContext from '../../helpers/WithDragDropContext';
+import { mockStoreWithTabs } from '../../fixtures/mockStore';
 
 describe('Header', () => {
   const props = {
@@ -43,6 +45,7 @@ describe('Header', () => {
     parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE),
     index: 0,
     editMode: false,
+    filters: {},
     handleComponentDrop() {},
     deleteComponent() {},
     updateComponents() {},
@@ -52,9 +55,11 @@ describe('Header', () => {
     // We have to wrap provide DragDropContext for the underlying DragDroppable
     // otherwise we cannot assert on DragDroppable children
     const wrapper = mount(
-      <WithDragDropContext>
-        <Header {...props} {...overrideProps} />
-      </WithDragDropContext>,
+      <Provider store={mockStoreWithTabs}>
+        <WithDragDropContext>
+          <Header {...props} {...overrideProps} />
+        </WithDragDropContext>
+      </Provider>,
     );
     return wrapper;
   }
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
index 59b067c..72d3a03 100644
--- a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
@@ -18,7 +18,7 @@
  */
 import { Provider } from 'react-redux';
 import React from 'react';
-import { mount } from 'enzyme';
+import { mount, shallow } from 'enzyme';
 import sinon from 'sinon';
 import { Tabs as BootstrapTabs, Tab as BootstrapTab } from 'react-bootstrap';
 
@@ -154,4 +154,20 @@ describe('Tabs', () => {
 
     expect(deleteComponent.callCount).toBe(1);
   });
+
+  it('should direct display direct-link tab', () => {
+    let wrapper = shallow(<Tabs {...props} />);
+    // default show first tab child
+    expect(wrapper.state('tabIndex')).toBe(0);
+
+    // display child in directPathToChild list
+    const directPathToChild = dashboardLayoutWithTabs.present.ROW_ID2.parents.slice();
+    const directLinkProps = {
+      ...props,
+      directPathToChild,
+    };
+
+    wrapper = shallow(<Tabs {...directLinkProps} />);
+    expect(wrapper.state('tabIndex')).toBe(1);
+  });
 });
diff --git a/superset/assets/src/dashboard/util/getDashboardUrl.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardData.js
similarity index 75%
copy from superset/assets/src/dashboard/util/getDashboardUrl.js
copy to superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardData.js
index 9b8cada..8fcc4d5 100644
--- a/superset/assets/src/dashboard/util/getDashboardUrl.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardData.js
@@ -16,9 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-/* eslint camelcase: 0 */
+import { dashboardLayout } from './mockDashboardLayout';
 
-export default function getDashboardUrl(pathname, filters = {}) {
-  const preselect_filters = encodeURIComponent(JSON.stringify(filters));
-  return `${pathname}?preselect_filters=${preselect_filters}`;
-}
+// mock the object to be posted to save_dash or copy_dash API
+export default {
+  css: '',
+  dashboard_title: 'Test 1',
+  default_filters: {},
+  expanded_slices: {},
+  positions: dashboardLayout.present,
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
index e4187b9..f2f965c 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
@@ -108,12 +108,14 @@ export const dashboardLayoutWithTabs = {
       id: 'TABS_ID',
       type: TABS_TYPE,
       children: ['TAB_ID', 'TAB_ID2'],
+      parents: ['ROOT_ID'],
     },
 
     TAB_ID: {
       id: 'TAB_ID',
       type: TAB_TYPE,
       children: ['ROW_ID'],
+      parents: ['ROOT_ID', 'TABS_ID'],
       meta: {
         text: 'tab1',
       },
@@ -122,7 +124,8 @@ export const dashboardLayoutWithTabs = {
     TAB_ID2: {
       id: 'TAB_ID2',
       type: TAB_TYPE,
-      children: [],
+      children: ['ROW_ID2'],
+      parents: ['ROOT_ID', 'TABS_ID'],
       meta: {
         text: 'tab2',
       },
@@ -131,6 +134,7 @@ export const dashboardLayoutWithTabs = {
     CHART_ID: {
       ...newComponentFactory(CHART_TYPE),
       id: 'CHART_ID',
+      parents: ['ROOT_ID', 'TABS_ID', 'TAB_ID', 'ROW_ID'],
       meta: {
         chartId,
         width: 3,
@@ -143,12 +147,33 @@ export const dashboardLayoutWithTabs = {
       ...newComponentFactory(ROW_TYPE),
       id: 'ROW_ID',
       children: ['CHART_ID'],
+      parents: ['ROOT_ID', 'TABS_ID', 'TAB_ID'],
+    },
+
+    CHART_ID2: {
+      ...newComponentFactory(CHART_TYPE),
+      id: 'CHART_ID2',
+      parents: ['ROOT_ID', 'TABS_ID', 'TAB_ID2', 'ROW_ID2'],
+      meta: {
+        chartId,
+        width: 3,
+        height: 10,
+        chartName: 'Mock chart name 2',
+      },
+    },
+
+    ROW_ID2: {
+      ...newComponentFactory(ROW_TYPE),
+      id: 'ROW_ID2',
+      children: ['CHART_ID2'],
+      parents: ['ROOT_ID', 'TABS_ID', 'TAB_ID2'],
     },
 
     [DASHBOARD_GRID_ID]: {
       type: DASHBOARD_GRID_TYPE,
       id: DASHBOARD_GRID_ID,
       children: [],
+      parents: ['ROOT_ID'],
       meta: {},
     },
 
diff --git a/superset/assets/spec/javascripts/dashboard/util/findTabIndexByComponentId_spec.js b/superset/assets/spec/javascripts/dashboard/util/findTabIndexByComponentId_spec.js
new file mode 100644
index 0000000..3e3d0f7
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/findTabIndexByComponentId_spec.js
@@ -0,0 +1,85 @@
+/**
+ * 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 findTabIndexByComponentId from '../../../../src/dashboard/util/findTabIndexByComponentId';
+
+describe('findTabIndexByComponentId', () => {
+  const topLevelTabsComponent = {
+    children: ['TAB-0g-5l347I2', 'TAB-qrwN_9VB5'],
+    id: 'TABS-MNQQSW-kyd',
+    meta: {},
+    parents: ['ROOT_ID'],
+    type: 'TABS',
+  };
+  const rowLevelTabsComponent = {
+    children: [
+      'TAB-TwyUUGp2Bg',
+      'TAB-Zl1BQAUvN',
+      'TAB-P0DllxzTU',
+      'TAB---e53RNei',
+    ],
+    id: 'TABS-Oduxop1L7I',
+    meta: {},
+    parents: ['ROOT_ID', 'TABS-MNQQSW-kyd', 'TAB-qrwN_9VB5'],
+    type: 'TABS',
+  };
+  const goodPathToChild = [
+    'ROOT_ID',
+    'TABS-MNQQSW-kyd',
+    'TAB-qrwN_9VB5',
+    'TABS-Oduxop1L7I',
+    'TAB-P0DllxzTU',
+    'ROW-JXhrFnVP8',
+    'CHART-dUIVg-ENq6',
+  ];
+  const badPath = ['ROOT_ID', 'TABS-MNQQSW-kyd', 'TAB-ABC', 'TABS-Oduxop1L7I'];
+
+  it('should return 0 if no directPathToChild', () => {
+    expect(
+      findTabIndexByComponentId({
+        currentComponent: topLevelTabsComponent,
+        directPathToChild: [],
+      }),
+    ).toBe(0);
+  });
+
+  it('should return 0 if not found tab id', () => {
+    expect(
+      findTabIndexByComponentId({
+        currentComponent: topLevelTabsComponent,
+        directPathToChild: badPath,
+      }),
+    ).toBe(0);
+  });
+
+  it('should return children index if matched an id in the path', () => {
+    expect(
+      findTabIndexByComponentId({
+        currentComponent: topLevelTabsComponent,
+        directPathToChild: goodPathToChild,
+      }),
+    ).toBe(1);
+
+    expect(
+      findTabIndexByComponentId({
+        currentComponent: rowLevelTabsComponent,
+        directPathToChild: goodPathToChild,
+      }),
+    ).toBe(2);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/updateComponentParentsList_spec.js b/superset/assets/spec/javascripts/dashboard/util/updateComponentParentsList_spec.js
new file mode 100644
index 0000000..d435f0d
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/updateComponentParentsList_spec.js
@@ -0,0 +1,97 @@
+/**
+ * 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 updateComponentParentsList from '../../../../src/dashboard/util/updateComponentParentsList';
+import { DASHBOARD_ROOT_ID } from '../../../../src/dashboard/util/constants';
+import {
+  dashboardLayout,
+  dashboardLayoutWithTabs,
+} from '../fixtures/mockDashboardLayout';
+
+describe('updateComponentParentsList', () => {
+  const emptyLayout = {
+    DASHBOARD_VERSION_KEY: 'v2',
+    GRID_ID: {
+      children: [],
+      id: 'GRID_ID',
+      type: 'GRID',
+    },
+    ROOT_ID: {
+      children: ['GRID_ID'],
+      id: 'ROOT_ID',
+      type: 'ROOT',
+    },
+  };
+  const gridLayout = {
+    ...dashboardLayout.present,
+  };
+  const tabsLayout = {
+    ...dashboardLayoutWithTabs.present,
+  };
+
+  it('should handle empty layout', () => {
+    const nextState = {
+      ...emptyLayout,
+    };
+
+    updateComponentParentsList({
+      currentComponent: nextState[DASHBOARD_ROOT_ID],
+      layout: nextState,
+    });
+
+    expect(nextState.GRID_ID.parents).toEqual(['ROOT_ID']);
+  });
+
+  it('should handle grid layout', () => {
+    const nextState = {
+      ...gridLayout,
+    };
+
+    updateComponentParentsList({
+      currentComponent: nextState[DASHBOARD_ROOT_ID],
+      layout: nextState,
+    });
+
+    expect(nextState.GRID_ID.parents).toEqual(['ROOT_ID']);
+    expect(nextState.CHART_ID.parents).toEqual([
+      'ROOT_ID',
+      'GRID_ID',
+      'ROW_ID',
+      'COLUMN_ID',
+    ]);
+  });
+
+  it('should handle root level tabs', () => {
+    const nextState = {
+      ...tabsLayout,
+    };
+
+    updateComponentParentsList({
+      currentComponent: nextState[DASHBOARD_ROOT_ID],
+      layout: nextState,
+    });
+
+    expect(nextState.GRID_ID.parents).toEqual(['ROOT_ID']);
+    expect(nextState.CHART_ID2.parents).toEqual([
+      'ROOT_ID',
+      'TABS_ID',
+      'TAB_ID2',
+      'ROW_ID2',
+    ]);
+  });
+});
diff --git a/superset/assets/src/components/AnchorLink.jsx b/superset/assets/src/components/AnchorLink.jsx
new file mode 100644
index 0000000..b33ee60
--- /dev/null
+++ b/superset/assets/src/components/AnchorLink.jsx
@@ -0,0 +1,99 @@
+/**
+ * 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 React from 'react';
+import PropTypes from 'prop-types';
+import { t } from '@superset-ui/translation';
+
+import URLShortLinkButton from './URLShortLinkButton';
+import getDashboardUrl from '../dashboard/util/getDashboardUrl';
+
+const propTypes = {
+  anchorLinkId: PropTypes.string.isRequired,
+  filters: PropTypes.object,
+  showShortLinkButton: PropTypes.bool,
+  placement: PropTypes.oneOf(['right', 'left', 'top', 'bottom']),
+};
+
+const defaultProps = {
+  showShortLinkButton: false,
+  placement: 'right',
+  filters: {},
+};
+
+
+class AnchorLink extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.handleClickAnchorLink = this.handleClickAnchorLink.bind(this);
+  }
+
+  componentDidMount() {
+    const hash = this.getLocationHash();
+    const { anchorLinkId } = this.props;
+
+    if (hash && anchorLinkId === hash) {
+      const directLinkComponent = document.getElementById(anchorLinkId);
+      if (directLinkComponent) {
+        setTimeout(() => {
+          directLinkComponent.scrollIntoView({
+            block: 'center',
+            behavior: 'smooth',
+          });
+        }, 1000);
+      }
+    }
+  }
+
+  getLocationHash() {
+    return (window.location.hash || '').substring(1);
+  }
+
+  handleClickAnchorLink(ev) {
+    ev.preventDefault();
+    history.pushState(null, null, `#${this.props.anchorLinkId}`);
+  }
+
+  render() {
+    const { anchorLinkId, filters, showShortLinkButton, placement } = this.props;
+    return (
+      <span
+        className="anchor-link-container"
+        id={anchorLinkId}
+      >
+        {showShortLinkButton &&
+        <URLShortLinkButton
+          url={getDashboardUrl(
+            window.location.pathname,
+            filters,
+            anchorLinkId,
+          )}
+          emailSubject={t('Superset Chart')}
+          emailContent={t('Check out this chart in dashboard:')}
+          placement={placement}
+        />}
+      </span>
+    );
+  }
+}
+
+AnchorLink.propTypes = propTypes;
+AnchorLink.defaultProps = defaultProps;
+
+export default AnchorLink;
diff --git a/superset/assets/src/components/URLShortLinkButton.jsx b/superset/assets/src/components/URLShortLinkButton.jsx
index 681992a..d6d6c16 100644
--- a/superset/assets/src/components/URLShortLinkButton.jsx
+++ b/superset/assets/src/components/URLShortLinkButton.jsx
@@ -29,6 +29,7 @@ const propTypes = {
   emailSubject: PropTypes.string,
   emailContent: PropTypes.string,
   addDangerToast: PropTypes.func.isRequired,
+  placement: PropTypes.oneOf(['right', 'left', 'top', 'bottom']),
 };
 
 class URLShortLinkButton extends React.Component {
@@ -73,7 +74,7 @@ class URLShortLinkButton extends React.Component {
         trigger="click"
         rootClose
         shouldUpdatePosition
-        placement="left"
+        placement={this.props.placement}
         onEnter={this.getCopyUrl}
         overlay={this.renderPopover()}
       >
@@ -87,6 +88,7 @@ class URLShortLinkButton extends React.Component {
 
 URLShortLinkButton.defaultProps = {
   url: window.location.href.substring(window.location.origin.length),
+  placement: 'left',
   emailSubject: '',
   emailContent: '',
 };
diff --git a/superset/assets/src/components/URLShortLinkModal.jsx b/superset/assets/src/components/URLShortLinkModal.jsx
index 2fc01c6..d6bf3f6 100644
--- a/superset/assets/src/components/URLShortLinkModal.jsx
+++ b/superset/assets/src/components/URLShortLinkModal.jsx
@@ -30,6 +30,7 @@ const propTypes = {
   emailContent: PropTypes.string,
   addDangerToast: PropTypes.func.isRequired,
   isMenuItem: PropTypes.bool,
+  title: PropTypes.string,
   triggerNode: PropTypes.node.isRequired,
 };
 
@@ -65,7 +66,7 @@ class URLShortLinkModal extends React.Component {
         isMenuItem={this.props.isMenuItem}
         triggerNode={this.props.triggerNode}
         beforeOpen={this.getCopyUrl}
-        modalTitle={t('Share Dashboard')}
+        modalTitle={this.props.title || t('Share Dashboard')}
         modalBody={
           <div>
             <CopyToClipboard
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index 5b163d9..23cb90e 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -219,3 +219,6 @@ export function undoLayoutAction() {
 export const redoLayoutAction = setUnsavedChangesAfterAction(
   UndoActionCreators.redo,
 );
+
+// Update component parents list ----------------------------------------------
+export const UPDATE_COMPONENTS_PARENTS_LIST = 'UPDATE_COMPONENTS_PARENTS_LIST';
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index 0864819..adf90c6 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -32,6 +32,7 @@ import {
   addWarningToast,
   addDangerToast,
 } from '../../messageToasts/actions';
+import { UPDATE_COMPONENTS_PARENTS_LIST } from '../actions/dashboardLayout';
 
 export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
 export function setUnsavedChanges(hasUnsavedChanges) {
@@ -139,19 +140,18 @@ export function saveDashboardRequestSuccess() {
 export function saveDashboardRequest(data, id, saveType) {
   const path = saveType === SAVE_TYPE_OVERWRITE ? 'save_dash' : 'copy_dash';
 
-  return dispatch =>
-    SupersetClient.post({
+  return dispatch => {
+    dispatch({ type: UPDATE_COMPONENTS_PARENTS_LIST });
+
+    return SupersetClient.post({
       endpoint: `/superset/${path}/${id}/`,
       postPayload: { data },
     })
-      .then(response =>
-        Promise.all([
-          dispatch(saveDashboardRequestSuccess()),
-          dispatch(
-            addSuccessToast(t('This dashboard was saved successfully.')),
-          ),
-        ]).then(() => Promise.resolve(response)),
-      )
+      .then(response => {
+        dispatch(saveDashboardRequestSuccess());
+        dispatch(addSuccessToast(t('This dashboard was saved successfully.')));
+        return response;
+      })
       .catch(response =>
         getClientErrorObject(response).then(({ error }) =>
           dispatch(
@@ -163,6 +163,7 @@ export function saveDashboardRequest(data, id, saveType) {
           ),
         ),
       );
+  };
 }
 
 export function fetchCharts(chartList = [], force = false, interval = 0) {
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index e635f90..345807d 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -36,6 +36,7 @@ import ToastPresenter from '../../messageToasts/containers/ToastPresenter';
 import WithPopoverMenu from './menu/WithPopoverMenu';
 
 import getDragDropManager from '../util/getDragDropManager';
+import findTabIndexByComponentId from '../util/findTabIndexByComponentId';
 
 import {
   DASHBOARD_GRID_ID,
@@ -54,10 +55,12 @@ const propTypes = {
   showBuilderPane: PropTypes.bool,
   handleComponentDrop: PropTypes.func.isRequired,
   toggleBuilderPane: PropTypes.func.isRequired,
+  directPathToChild: PropTypes.arrayOf(PropTypes.string),
 };
 
 const defaultProps = {
   showBuilderPane: false,
+  directPathToChild: [],
 };
 
 class DashboardBuilder extends React.Component {
@@ -72,8 +75,19 @@ class DashboardBuilder extends React.Component {
 
   constructor(props) {
     super(props);
+
+    const { dashboardLayout, directPathToChild } = props;
+    const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
+    const rootChildId = dashboardRoot.children[0];
+    const topLevelTabs =
+      rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
+    const tabIndex = findTabIndexByComponentId({
+      currentComponent: topLevelTabs || dashboardLayout[DASHBOARD_ROOT_ID],
+      directPathToChild,
+    });
+
     this.state = {
-      tabIndex: 0, // top-level tabs
+      tabIndex,
     };
     this.handleChangeTab = this.handleChangeTab.bind(this);
     this.handleDeleteTopLevelTabs = this.handleDeleteTopLevelTabs.bind(this);
diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
index 4c36d9a..7e10d4c 100644
--- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
+++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -180,7 +180,11 @@ class HeaderActionsDropdown extends React.PureComponent {
         )}
 
         <URLShortLinkModal
-          url={getDashboardUrl(window.location.pathname, this.props.filters)}
+          url={getDashboardUrl(
+            window.location.pathname,
+            this.props.filters,
+            window.location.hash,
+          )}
           emailSubject={emailSubject}
           emailContent={emailBody}
           addDangerToast={this.props.addDangerToast}
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index 913a00d..723ae3e 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -43,6 +43,9 @@ const propTypes = {
   supersetCanExplore: PropTypes.bool,
   supersetCanCSV: PropTypes.bool,
   sliceCanEdit: PropTypes.bool,
+  componentId: PropTypes.string.isRequired,
+  filters: PropTypes.object.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -90,6 +93,9 @@ class SliceHeader extends React.PureComponent {
       updateSliceName,
       annotationQuery,
       annotationError,
+      componentId,
+      filters,
+      addDangerToast,
     } = this.props;
 
     return (
@@ -138,6 +144,9 @@ class SliceHeader extends React.PureComponent {
               supersetCanExplore={supersetCanExplore}
               supersetCanCSV={supersetCanCSV}
               sliceCanEdit={sliceCanEdit}
+              componentId={componentId}
+              filters={filters}
+              addDangerToast={addDangerToast}
             />
           )}
         </div>
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index 18a9207..65565f0 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -21,9 +21,14 @@ import PropTypes from 'prop-types';
 import moment from 'moment';
 import { Dropdown, MenuItem } from 'react-bootstrap';
 import { t } from '@superset-ui/translation';
+import URLShortLinkModal from '../../components/URLShortLinkModal';
+import getDashboardUrl from '../util/getDashboardUrl';
 
 const propTypes = {
   slice: PropTypes.object.isRequired,
+  componentId: PropTypes.string.isRequired,
+  filters: PropTypes.object.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
   isCached: PropTypes.bool,
   isExpanded: PropTypes.bool,
   cachedDttm: PropTypes.string,
@@ -97,7 +102,15 @@ class SliceHeaderControls extends React.PureComponent {
   }
 
   render() {
-    const { slice, isCached, cachedDttm, updatedDttm } = this.props;
+    const {
+      slice,
+      isCached,
+      cachedDttm,
+      updatedDttm,
+      filters,
+      componentId,
+      addDangerToast,
+    } = this.props;
     const cachedWhen = moment.utc(cachedDttm).fromNow();
     const updatedWhen = updatedDttm ? moment.utc(updatedDttm).fromNow() : '';
     const refreshTooltip = isCached
@@ -145,6 +158,18 @@ class SliceHeaderControls extends React.PureComponent {
               {t('Explore chart')}
             </MenuItem>
           )}
+
+          <URLShortLinkModal
+            url={getDashboardUrl(
+              window.location.pathname,
+              filters,
+              componentId,
+            )}
+            addDangerToast={addDangerToast}
+            isMenuItem
+            title={t('Share chart')}
+            triggerNode={<span>{t('Share chart')}</span>}
+          />
         </Dropdown.Menu>
       </Dropdown>
     );
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index d8b01d0..ff11208 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -33,6 +33,7 @@ import {
 
 const propTypes = {
   id: PropTypes.number.isRequired,
+  componentId: PropTypes.string.isRequired,
   width: PropTypes.number.isRequired,
   height: PropTypes.number.isRequired,
   updateSliceName: PropTypes.func.isRequired,
@@ -55,6 +56,7 @@ const propTypes = {
   supersetCanExplore: PropTypes.bool.isRequired,
   supersetCanCSV: PropTypes.bool.isRequired,
   sliceCanEdit: PropTypes.bool.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -184,6 +186,7 @@ class Chart extends React.Component {
   render() {
     const {
       id,
+      componentId,
       chart,
       slice,
       datasource,
@@ -198,6 +201,7 @@ class Chart extends React.Component {
       supersetCanExplore,
       supersetCanCSV,
       sliceCanEdit,
+      addDangerToast,
     } = this.props;
 
     const { width } = this.state;
@@ -233,6 +237,9 @@ class Chart extends React.Component {
           supersetCanExplore={supersetCanExplore}
           supersetCanCSV={supersetCanCSV}
           sliceCanEdit={sliceCanEdit}
+          componentId={componentId}
+          filters={filters}
+          addDangerToast={addDangerToast}
         />
 
         {/*
diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index 56e2918..836c0e7 100644
--- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -20,6 +20,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import Chart from '../../containers/Chart';
+import AnchorLink from '../../../components/AnchorLink';
 import DeleteComponentButton from '../DeleteComponentButton';
 import DragDroppable from '../dnd/DragDroppable';
 import HoverMenu from '../menu/HoverMenu';
@@ -148,7 +149,9 @@ class ChartHolder extends React.Component {
               ref={dragSourceRef}
               className="dashboard-component dashboard-component-chart-holder"
             >
+              {!editMode && <AnchorLink anchorLinkId={component.id} />}
               <Chart
+                componentId={component.id}
                 id={component.meta.chartId}
                 width={Math.floor(
                   widthMultiple * columnWidth +
diff --git a/superset/assets/src/dashboard/components/gridComponents/Header.jsx b/superset/assets/src/dashboard/components/gridComponents/Header.jsx
index e7f2e42..e45b393 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Header.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Header.jsx
@@ -23,6 +23,7 @@ import cx from 'classnames';
 import DragDroppable from '../dnd/DragDroppable';
 import DragHandle from '../dnd/DragHandle';
 import EditableTitle from '../../../components/EditableTitle';
+import AnchorLink from '../../../components/AnchorLink';
 import HoverMenu from '../menu/HoverMenu';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
@@ -41,6 +42,7 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
+  filters: PropTypes.object.isRequired,
 
   // redux
   handleComponentDrop: PropTypes.func.isRequired,
@@ -101,6 +103,7 @@ class Header extends React.PureComponent {
       index,
       handleComponentDrop,
       editMode,
+      filters,
     } = this.props;
 
     const headerStyle = headerStyleOptions.find(
@@ -165,6 +168,13 @@ class Header extends React.PureComponent {
                   onSaveTitle={this.handleChangeText}
                   showTooltip={false}
                 />
+                {!editMode && (
+                  <AnchorLink
+                    anchorLinkId={component.id}
+                    filters={filters}
+                    showShortLinkButton
+                  />
+                )}
               </div>
             </WithPopoverMenu>
 
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 764702d..49a0f18 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -22,6 +22,7 @@ import PropTypes from 'prop-types';
 import DashboardComponent from '../../containers/DashboardComponent';
 import DragDroppable from '../dnd/DragDroppable';
 import EditableTitle from '../../../components/EditableTitle';
+import AnchorLink from '../../../components/AnchorLink';
 import DeleteComponentModal from '../DeleteComponentModal';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
@@ -41,6 +42,7 @@ const propTypes = {
   onDropOnTab: PropTypes.func,
   onDeleteTab: PropTypes.func,
   editMode: PropTypes.bool.isRequired,
+  filters: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number,
@@ -195,7 +197,14 @@ export default class Tab extends React.PureComponent {
 
   renderTab() {
     const { isFocused } = this.state;
-    const { component, parentComponent, index, depth, editMode } = this.props;
+    const {
+      component,
+      parentComponent,
+      index,
+      depth,
+      editMode,
+      filters,
+    } = this.props;
     const deleteTabIcon = (
       <div className="icon-button">
         <span className="fa fa-trash" />
@@ -238,6 +247,14 @@ export default class Tab extends React.PureComponent {
                 onSaveTitle={this.handleChangeText}
                 showTooltip={false}
               />
+              {!editMode && (
+                <AnchorLink
+                  anchorLinkId={component.id}
+                  filters={filters}
+                  showShortLinkButton
+                  placement={index >= 5 ? 'left' : 'right'}
+                />
+              )}
             </WithPopoverMenu>
 
             {dropIndicatorProps && <div {...dropIndicatorProps} />}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
index dc9d599..2b8934e 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
@@ -25,6 +25,7 @@ import DragHandle from '../dnd/DragHandle';
 import DashboardComponent from '../../containers/DashboardComponent';
 import DeleteComponentButton from '../DeleteComponentButton';
 import HoverMenu from '../menu/HoverMenu';
+import findTabIndexByComponentId from '../../util/findTabIndexByComponentId';
 import { componentShape } from '../../util/propShapes';
 import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
 import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
@@ -45,6 +46,7 @@ const propTypes = {
   editMode: PropTypes.bool.isRequired,
   renderHoverMenu: PropTypes.bool,
   logEvent: PropTypes.func.isRequired,
+  directPathToChild: PropTypes.arrayOf(PropTypes.string),
 
   // grid related
   availableColumnCount: PropTypes.number,
@@ -67,6 +69,7 @@ const defaultProps = {
   renderHoverMenu: true,
   availableColumnCount: 0,
   columnWidth: 0,
+  directPathToChild: [],
   onChangeTab() {},
   onResizeStart() {},
   onResize() {},
@@ -76,8 +79,13 @@ const defaultProps = {
 class Tabs extends React.PureComponent {
   constructor(props) {
     super(props);
+    const tabIndex = findTabIndexByComponentId({
+      currentComponent: props.component,
+      directPathToChild: props.directPathToChild,
+    });
+
     this.state = {
-      tabIndex: 0,
+      tabIndex,
     };
     this.handleClickTab = this.handleClickTab.bind(this);
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 1e0e64c..5b27b13 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -23,10 +23,11 @@ import {
   changeFilter as addFilter,
   toggleExpandSlice,
 } from '../actions/dashboardState';
+import { updateComponents } from '../actions/dashboardLayout';
+import { addDangerToast } from '../../messageToasts/actions';
 import { refreshChart } from '../../chart/chartAction';
 import { logEvent } from '../../logger/actions';
 import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
-import { updateComponents } from '../actions/dashboardLayout';
 import Chart from '../components/gridComponents/Chart';
 
 const EMPTY_FILTERS = {};
@@ -72,6 +73,7 @@ function mapDispatchToProps(dispatch) {
   return bindActionCreators(
     {
       updateComponents,
+      addDangerToast,
       toggleExpandSlice,
       addFilter,
       refreshChart,
diff --git a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
index 3ca7043..9e1804f 100644
--- a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
@@ -31,6 +31,7 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) {
     dashboardLayout: undoableLayout.present,
     editMode: dashboardState.editMode,
     showBuilderPane: dashboardState.showBuilderPane,
+    directPathToChild: dashboardState.directPathToChild,
   };
 }
 
diff --git a/superset/assets/src/dashboard/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
index 180fcd3..a1a1c37 100644
--- a/superset/assets/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
@@ -43,6 +43,11 @@ const propTypes = {
   updateComponents: PropTypes.func.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
   logEvent: PropTypes.func.isRequired,
+  directPathToChild: PropTypes.arrayOf(PropTypes.string),
+};
+
+const defaultProps = {
+  directPathToChild: [],
 };
 
 function mapStateToProps(
@@ -56,6 +61,8 @@ function mapStateToProps(
     component,
     parentComponent: dashboardLayout[parentId],
     editMode: dashboardState.editMode,
+    filters: dashboardState.filters,
+    directPathToChild: dashboardState.directPathToChild,
   };
 
   // rows and columns need more data about their child dimensions
@@ -98,6 +105,7 @@ class DashboardComponent extends React.PureComponent {
 }
 
 DashboardComponent.propTypes = propTypes;
+DashboardComponent.defaultProps = defaultProps;
 
 export default connect(
   mapStateToProps,
diff --git a/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js b/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
index f5799b0..d693aca 100644
--- a/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
+++ b/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
@@ -39,6 +39,7 @@ export default {
     type: DASHBOARD_GRID_TYPE,
     id: DASHBOARD_GRID_ID,
     children: [],
+    parents: [DASHBOARD_ROOT_ID],
     meta: {},
   },
 
diff --git a/superset/assets/src/dashboard/reducers/dashboardLayout.js b/superset/assets/src/dashboard/reducers/dashboardLayout.js
index c880a24..23459e0 100644
--- a/superset/assets/src/dashboard/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -24,6 +24,7 @@ import {
 import componentIsResizable from '../util/componentIsResizable';
 import findParentId from '../util/findParentId';
 import getComponentWidthFromDrop from '../util/getComponentWidthFromDrop';
+import updateComponentParentsList from '../util/updateComponentParentsList';
 import newComponentFactory from '../util/newComponentFactory';
 import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
 import reorderItem from '../util/dnd-reorder';
@@ -32,6 +33,7 @@ import { ROW_TYPE, TAB_TYPE, TABS_TYPE } from '../util/componentTypes';
 
 import {
   UPDATE_COMPONENTS,
+  UPDATE_COMPONENTS_PARENTS_LIST,
   DELETE_COMPONENT,
   CREATE_COMPONENT,
   MOVE_COMPONENT,
@@ -255,6 +257,21 @@ const actionHandlers = {
 
     return nextEntities;
   },
+
+  [UPDATE_COMPONENTS_PARENTS_LIST](state) {
+    const nextState = {
+      ...state,
+    };
+
+    updateComponentParentsList({
+      currentComponent: nextState[DASHBOARD_ROOT_ID],
+      layout: nextState,
+    });
+
+    return {
+      ...nextState,
+    };
+  },
 };
 
 export default function layoutReducer(state = {}, action) {
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index a9c4d8f..44a491c 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -63,7 +63,11 @@ export default function(bootstrapData) {
 
   // dashboard layout
   const { position_json: positionJson } = dashboard;
-  const layout = positionJson || getEmptyLayout();
+  // new dash: positionJson could be {} or null
+  const layout =
+    positionJson && Object.keys(positionJson).length > 0
+      ? positionJson
+      : getEmptyLayout();
 
   // create a lookup to sync layout names with slice names
   const chartIdToLayoutId = {};
@@ -155,6 +159,14 @@ export default function(bootstrapData) {
     future: [],
   };
 
+  // find direct link component and path from root
+  const directLinkComponentId = (window.location.hash || '#').substring(1);
+  let directPathToChild = [];
+  if (layout[directLinkComponentId]) {
+    directPathToChild = (layout[directLinkComponentId].parents || []).slice();
+    directPathToChild.push(directLinkComponentId);
+  }
+
   return {
     datasources,
     sliceEntities: { ...initSliceEntities, slices, isLoading: false },
@@ -185,6 +197,7 @@ export default function(bootstrapData) {
       sliceIds: Array.from(sliceIds),
       refresh: false,
       filters,
+      directPathToChild,
       expandedSlices: dashboard.metadata.expanded_slices || {},
       refreshFrequency: dashboard.metadata.refresh_frequency || 0,
       css: dashboard.css || '',
diff --git a/superset/assets/src/dashboard/util/getEmptyLayout.js b/superset/assets/src/dashboard/util/findTabIndexByComponentId.js
similarity index 57%
copy from superset/assets/src/dashboard/util/getEmptyLayout.js
copy to superset/assets/src/dashboard/util/findTabIndexByComponentId.js
index 28d3187..eed141e 100644
--- a/superset/assets/src/dashboard/util/getEmptyLayout.js
+++ b/superset/assets/src/dashboard/util/findTabIndexByComponentId.js
@@ -16,26 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DASHBOARD_ROOT_TYPE, DASHBOARD_GRID_TYPE } from './componentTypes';
+export default function findTabIndexByComponentId({
+  currentComponent,
+  directPathToChild = [],
+}) {
+  if (
+    !currentComponent ||
+    directPathToChild.length === 0 ||
+    directPathToChild.indexOf(currentComponent.id) === -1
+  ) {
+    return 0;
+  }
 
-import {
-  DASHBOARD_GRID_ID,
-  DASHBOARD_ROOT_ID,
-  DASHBOARD_VERSION_KEY,
-} from './constants';
-
-export default function() {
-  return {
-    [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: [],
-    },
-  };
+  const currentComponentIdx = directPathToChild.findIndex(
+    id => id === currentComponent.id,
+  );
+  const nextParentId = directPathToChild[currentComponentIdx + 1];
+  if (currentComponent.children.indexOf(nextParentId) >= 0) {
+    return currentComponent.children.findIndex(
+      childId => childId === nextParentId,
+    );
+  }
+  return 0;
 }
diff --git a/superset/assets/src/dashboard/util/getDashboardUrl.js b/superset/assets/src/dashboard/util/getDashboardUrl.js
index 9b8cada..243c8cb 100644
--- a/superset/assets/src/dashboard/util/getDashboardUrl.js
+++ b/superset/assets/src/dashboard/util/getDashboardUrl.js
@@ -18,7 +18,8 @@
  */
 /* eslint camelcase: 0 */
 
-export default function getDashboardUrl(pathname, filters = {}) {
+export default function getDashboardUrl(pathname, filters = {}, hash = '') {
   const preselect_filters = encodeURIComponent(JSON.stringify(filters));
-  return `${pathname}?preselect_filters=${preselect_filters}`;
+  const hashSection = hash ? `#${hash}` : '';
+  return `${pathname}?preselect_filters=${preselect_filters}${hashSection}`;
 }
diff --git a/superset/assets/src/dashboard/util/getEmptyLayout.js b/superset/assets/src/dashboard/util/getEmptyLayout.js
index 28d3187..a58866c 100644
--- a/superset/assets/src/dashboard/util/getEmptyLayout.js
+++ b/superset/assets/src/dashboard/util/getEmptyLayout.js
@@ -36,6 +36,7 @@ export default function() {
       type: DASHBOARD_GRID_TYPE,
       id: DASHBOARD_GRID_ID,
       children: [],
+      parents: [DASHBOARD_ROOT_ID],
     },
   };
 }
diff --git a/superset/assets/src/dashboard/util/getEmptyLayout.js b/superset/assets/src/dashboard/util/updateComponentParentsList.js
similarity index 61%
copy from superset/assets/src/dashboard/util/getEmptyLayout.js
copy to superset/assets/src/dashboard/util/updateComponentParentsList.js
index 28d3187..48f01e2 100644
--- a/superset/assets/src/dashboard/util/getEmptyLayout.js
+++ b/superset/assets/src/dashboard/util/updateComponentParentsList.js
@@ -16,26 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DASHBOARD_ROOT_TYPE, DASHBOARD_GRID_TYPE } from './componentTypes';
+export default function updateComponentParentsList({
+  currentComponent,
+  layout = {},
+}) {
+  if (currentComponent && layout[currentComponent.id]) {
+    const parentsList = (currentComponent.parents || []).slice();
+    parentsList.push(currentComponent.id);
 
-import {
-  DASHBOARD_GRID_ID,
-  DASHBOARD_ROOT_ID,
-  DASHBOARD_VERSION_KEY,
-} from './constants';
-
-export default function() {
-  return {
-    [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: [],
-    },
-  };
+    currentComponent.children.forEach(childId => {
+      layout[childId].parents = parentsList; // eslint-disable-line no-param-reassign
+      updateComponentParentsList({
+        currentComponent: layout[childId],
+        layout,
+      });
+    });
+  }
 }
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 587851e..c23d711 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -291,6 +291,51 @@ table.table-no-hover tr:hover {
   cursor: text;
 }
 
+.anchor-link-container {
+  position: absolute;
+  z-index: 5;
+
+  .btn.btn-sm, .btn.btn-sm:active {
+    border: none;
+    padding-top: 0;
+    padding-bottom: 0;
+    background: none;
+    box-shadow: none;
+  }
+
+  .fa.fa-link {
+    position: relative;
+    top: 2px;
+    right: 0;
+    visibility: hidden;
+    font-size: 11px;
+    text-align: center;
+    vertical-align: middle;
+  }
+}
+
+.nav.nav-tabs li .anchor-link-container {
+  top: 0;
+  right: -32px;
+}
+
+.dashboard-component.dashboard-component-header .anchor-link-container {
+  .fa.fa-link {
+    font-size: 16px;
+  }
+}
+
+.nav.nav-tabs li:hover,
+.dashboard-component.dashboard-component-header:hover {
+  .anchor-link-container {
+    cursor: pointer;
+
+    .fa.fa-link {
+      visibility: visible;
+    }
+  }
+}
+
 .m-r-5 {
   margin-right: 5px;
 }


Mime
View raw message