mirror of https://github.com/apache/superset.git
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
This commit is contained in:
parent
139f299ab3
commit
c50e6bc981
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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: {},
|
||||
},
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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: '',
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
||||
{/*
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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} />}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -31,6 +31,7 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) {
|
|||
dashboardLayout: undoableLayout.present,
|
||||
editMode: dashboardState.editMode,
|
||||
showBuilderPane: dashboardState.showBuilderPane,
|
||||
directPathToChild: dashboardState.directPathToChild,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -39,6 +39,7 @@ export default {
|
|||
type: DASHBOARD_GRID_TYPE,
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: [],
|
||||
parents: [DASHBOARD_ROOT_ID],
|
||||
meta: {},
|
||||
},
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 || '',
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
export default function findTabIndexByComponentId({
|
||||
currentComponent,
|
||||
directPathToChild = [],
|
||||
}) {
|
||||
if (
|
||||
!currentComponent ||
|
||||
directPathToChild.length === 0 ||
|
||||
directPathToChild.indexOf(currentComponent.id) === -1
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ export default function() {
|
|||
type: DASHBOARD_GRID_TYPE,
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: [],
|
||||
parents: [DASHBOARD_ROOT_ID],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
export default function updateComponentParentsList({
|
||||
currentComponent,
|
||||
layout = {},
|
||||
}) {
|
||||
if (currentComponent && layout[currentComponent.id]) {
|
||||
const parentsList = (currentComponent.parents || []).slice();
|
||||
parentsList.push(currentComponent.id);
|
||||
|
||||
currentComponent.children.forEach(childId => {
|
||||
layout[childId].parents = parentsList; // eslint-disable-line no-param-reassign
|
||||
updateComponentParentsList({
|
||||
currentComponent: layout[childId],
|
||||
layout,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue