Skip to content

Commit

Permalink
feat(dashboard): direct link to single chart/tab/header in dashboard (#…
Browse files Browse the repository at this point in the history
…6964)

* direct display for pre-selected tab

* update parents

* add AnchorLink component

* add unit tests
  • Loading branch information
Grace Guo authored Apr 9, 2019
1 parent 139f299 commit c50e6bc
Show file tree
Hide file tree
Showing 33 changed files with 813 additions and 26 deletions.
63 changes: 63 additions & 0 deletions superset/assets/spec/javascripts/components/AnchorLink_spec.jsx
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/
import React from 'react';
import { Provider } from 'react-redux';
import { mount } from 'enzyme';
import sinon from 'sinon';

Expand All @@ -33,6 +34,7 @@ import {
} from '../../../../../src/dashboard/util/componentTypes';

import WithDragDropContext from '../../helpers/WithDragDropContext';
import { mockStoreWithTabs } from '../../fixtures/mockStore';

describe('Header', () => {
const props = {
Expand All @@ -43,6 +45,7 @@ describe('Header', () => {
parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE),
index: 0,
editMode: false,
filters: {},
handleComponentDrop() {},
deleteComponent() {},
updateComponents() {},
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 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 { dashboardLayout } from './mockDashboardLayout';

// 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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand All @@ -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',
},
Expand All @@ -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,
Expand All @@ -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: {},
},

Expand Down
Loading

0 comments on commit c50e6bc

Please sign in to comment.