diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/controls.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/controls.test.js
index 58849de90b..6f9215ce24 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/controls.test.js
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/controls.test.js
@@ -72,9 +72,10 @@ describe('Dashboard top-level controls', () => {
.trigger('click', { force: true });
// not allow dashboard level force refresh when any chart is loading
- cy.get('[data-test="refresh-dashboard-menu-item"]')
- .parent()
- .should('have.class', 'disabled');
+ cy.get('[data-test="refresh-dashboard-menu-item"]').should(
+ 'have.class',
+ 'ant-menu-item-disabled',
+ );
// not allow chart level force refresh when it is loading
cy.get(`#slice_${mapId}-controls`)
.next()
@@ -84,25 +85,28 @@ describe('Dashboard top-level controls', () => {
.should('have.class', 'disabled');
cy.wait(`@postJson_${mapId}_force`);
- cy.get('[data-test="refresh-dashboard-menu-item"]')
- .parent()
- .not('have.class', 'disabled');
+ cy.get('[data-test="refresh-dashboard-menu-item"]').should(
+ 'not.have.class',
+ 'ant-menu-item-disabled',
+ );
});
it('should allow dashboard level force refresh', () => {
// when charts are not start loading, for example, under a secondary tab,
// should allow force refresh
cy.get('[data-test="more-horiz"]').click();
- cy.get('[data-test="refresh-dashboard-menu-item"]')
- .parent()
- .not('have.class', 'disabled');
+ cy.get('[data-test="refresh-dashboard-menu-item"]').should(
+ 'not.have.class',
+ 'ant-menu-item-disabled',
+ );
// wait the all dash finish loading.
cy.wait(sliceRequests);
cy.get('[data-test="refresh-dashboard-menu-item"]').click();
- cy.get('[data-test="refresh-dashboard-menu-item"]')
- .parent()
- .should('have.class', 'disabled');
+ cy.get('[data-test="refresh-dashboard-menu-item"]').should(
+ 'have.class',
+ 'ant-menu-item-disabled',
+ );
// wait all charts force refreshed
cy.wait(forceRefreshRequests, { responseTimeout: 15000 }).then(xhrs => {
@@ -115,8 +119,9 @@ describe('Dashboard top-level controls', () => {
});
cy.get('[data-test="more-horiz"]').click();
- cy.get('[data-test="refresh-dashboard-menu-item"]')
- .parent()
- .not('have.class', 'disabled');
+ cy.get('[data-test="refresh-dashboard-menu-item"]').should(
+ 'not.have.class',
+ 'ant-menu-item-disabled',
+ );
});
});
diff --git a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
index 033e801bc5..deb9450452 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
@@ -18,7 +18,8 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
-import { DropdownButton, MenuItem } from 'react-bootstrap';
+import { DropdownButton } from 'react-bootstrap';
+import { Menu } from 'src/common/components';
import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
import URLShortLinkModal from 'src/components/URLShortLinkModal';
import HeaderActionsDropdown from 'src/dashboard/components/HeaderActionsDropdown';
@@ -72,9 +73,9 @@ describe('HeaderActionsDropdown', () => {
expect(wrapper.find(SaveModal)).not.toExist();
});
- it('should render four MenuItems', () => {
+ it('should render five Menu items', () => {
const wrapper = setup(overrideProps);
- expect(wrapper.find(MenuItem)).toHaveLength(4);
+ expect(wrapper.find(Menu.Item)).toHaveLength(5);
});
it('should render the RefreshIntervalModal', () => {
@@ -106,9 +107,9 @@ describe('HeaderActionsDropdown', () => {
expect(wrapper.find(SaveModal)).toExist();
});
- it('should render four MenuItems', () => {
+ it('should render six Menu items', () => {
const wrapper = setup(overrideProps);
- expect(wrapper.find(MenuItem)).toHaveLength(4);
+ expect(wrapper.find(Menu.Item)).toHaveLength(6);
});
it('should render the RefreshIntervalModal', () => {
@@ -140,9 +141,9 @@ describe('HeaderActionsDropdown', () => {
expect(wrapper.find(SaveModal)).toExist();
});
- it('should render three MenuItems', () => {
+ it('should render seven MenuItems', () => {
const wrapper = setup(overrideProps);
- expect(wrapper.find(MenuItem)).toHaveLength(3);
+ expect(wrapper.find(Menu.Item)).toHaveLength(7);
});
it('should render the RefreshIntervalModal', () => {
diff --git a/superset-frontend/src/common/components/index.tsx b/superset-frontend/src/common/components/index.tsx
index 2e6b90d9e3..99fafb3dd3 100644
--- a/superset-frontend/src/common/components/index.tsx
+++ b/superset-frontend/src/common/components/index.tsx
@@ -33,8 +33,10 @@ export {
DatePicker,
Dropdown,
Empty,
+ Input,
Modal,
Popover,
+ Select,
Skeleton,
Tabs,
Tooltip,
diff --git a/superset-frontend/src/components/CopyToClipboard.jsx b/superset-frontend/src/components/CopyToClipboard.jsx
index 918e3608c2..abc7df60c8 100644
--- a/superset-frontend/src/components/CopyToClipboard.jsx
+++ b/superset-frontend/src/components/CopyToClipboard.jsx
@@ -18,7 +18,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
-import { Tooltip, OverlayTrigger, MenuItem } from 'react-bootstrap';
+import { Tooltip, OverlayTrigger } from 'react-bootstrap';
import { t } from '@superset-ui/core';
import withToasts from 'src/messageToasts/enhancers/withToasts';
@@ -28,7 +28,6 @@ const propTypes = {
onCopyEnd: PropTypes.func,
shouldShowText: PropTypes.bool,
text: PropTypes.string,
- inMenu: PropTypes.bool,
wrapped: PropTypes.bool,
tooltipText: PropTypes.string,
addDangerToast: PropTypes.func.isRequired,
@@ -38,7 +37,6 @@ const defaultProps = {
copyNode: Copy,
onCopyEnd: () => {},
shouldShowText: true,
- inMenu: false,
wrapped: true,
tooltipText: t('Copy to clipboard'),
};
@@ -160,27 +158,6 @@ class CopyToClipboard extends React.Component {
);
}
- renderInMenu() {
- return (
-
-
-
- );
- }
-
renderTooltip() {
return (
{this.tooltipText()}
@@ -188,11 +165,11 @@ class CopyToClipboard extends React.Component {
}
render() {
- const { wrapped, inMenu } = this.props;
+ const { wrapped } = this.props;
if (!wrapped) {
return this.renderNotWrapped();
}
- return inMenu ? this.renderInMenu() : this.renderLink();
+ return this.renderLink();
}
}
diff --git a/superset-frontend/src/components/Menu/LanguagePicker.tsx b/superset-frontend/src/components/Menu/LanguagePicker.tsx
index 69e2846313..51b88be89f 100644
--- a/superset-frontend/src/components/Menu/LanguagePicker.tsx
+++ b/superset-frontend/src/components/Menu/LanguagePicker.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
-import { MenuItem } from 'react-bootstrap';
+import { Menu } from 'src/common/components';
import NavDropdown from 'src/components/NavDropdown';
export interface Languages {
@@ -46,17 +46,23 @@ export default function LanguagePicker({
}
>
- {Object.keys(languages).map(langKey =>
- langKey === locale ? null : (
-
- ),
- )}
+
);
}
diff --git a/superset-frontend/src/dashboard/components/CssEditor.jsx b/superset-frontend/src/dashboard/components/CssEditor.jsx
index 69f2500233..396276ff1c 100644
--- a/superset-frontend/src/dashboard/components/CssEditor.jsx
+++ b/superset-frontend/src/dashboard/components/CssEditor.jsx
@@ -81,7 +81,6 @@ class CssEditor extends React.PureComponent {
{this.renderTemplateSelector()}
diff --git a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
index 35f9a3085c..ede085f4d9 100644
--- a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
+++ b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -20,8 +20,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { SupersetClient, t } from '@superset-ui/core';
-import { DropdownButton, MenuItem } from 'react-bootstrap';
+import { DropdownButton } from 'react-bootstrap';
+import { Menu } from 'src/common/components';
import Icon from 'src/components/Icon';
import CssEditor from './CssEditor';
@@ -71,6 +72,18 @@ const defaultProps = {
refreshWarning: null,
};
+const MENU_KEYS = {
+ SAVE_MODAL: 'save-modal',
+ SHARE_DASHBOARD: 'share-dashboard',
+ REFRESH_DASHBOARD: 'refresh-dashboard',
+ AUTOREFRESH_MODAL: 'autorefresh-modal',
+ SET_FILTER_MAPPING: 'set-filter-mapping',
+ EDIT_PROPERTIES: 'edit-properties',
+ EDIT_CSS: 'edit-css',
+ DOWNLOAD_AS_IMAGE: 'download-as-image',
+ TOGGLE_FULLSCREEN: 'toggle-fullscreen',
+};
+
class HeaderActionsDropdown extends React.PureComponent {
static discardChanges() {
window.location.reload();
@@ -85,6 +98,7 @@ class HeaderActionsDropdown extends React.PureComponent {
this.changeCss = this.changeCss.bind(this);
this.changeRefreshInterval = this.changeRefreshInterval.bind(this);
+ this.handleMenuClick = this.handleMenuClick.bind(this);
}
UNSAFE_componentWillMount() {
@@ -119,12 +133,40 @@ class HeaderActionsDropdown extends React.PureComponent {
this.props.startPeriodicRender(refreshInterval * 1000);
}
+ handleMenuClick({ key, domEvent }) {
+ switch (key) {
+ case MENU_KEYS.REFRESH_DASHBOARD:
+ this.props.forceRefreshAllCharts();
+ break;
+ case MENU_KEYS.EDIT_PROPERTIES:
+ this.props.showPropertiesModal();
+ break;
+ case MENU_KEYS.DOWNLOAD_AS_IMAGE:
+ downloadAsImage('.dashboard', this.props.dashboardTitle)(domEvent);
+ break;
+ case MENU_KEYS.TOGGLE_FULLSCREEN: {
+ const hasStandalone = window.location.search.includes(
+ 'standalone=true',
+ );
+ const url = getDashboardUrl(
+ window.location.pathname,
+ getActiveFilters(),
+ window.location.hash,
+ !hasStandalone,
+ );
+ window.location.replace(url);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
render() {
const {
dashboardTitle,
dashboardId,
dashboardInfo,
- forceRefreshAllCharts,
refreshFrequency,
shouldPersistRefreshFrequency,
editMode,
@@ -155,105 +197,102 @@ class HeaderActionsDropdown extends React.PureComponent {
style={{ border: 'none', padding: 0, marginLeft: '4px' }}
pullRight
>
- {userCanSave && (
- <>
- {t('Save as')}
- }
- canOverwrite={userCanEdit}
- />
- >
- )}
-
+ {userCanSave && (
+
+ {t('Save as')}
+ }
+ canOverwrite={userCanEdit}
+ />
+
)}
- emailSubject={emailSubject}
- emailContent={emailBody}
- addDangerToast={this.props.addDangerToast}
- isMenuItem
- triggerNode={{t('Share dashboard')}}
- />
-
-
- {t('Set auto-refresh interval')}}
- />
-
- {editMode && (
- <>
- {t('Set filter mapping')}
- }
- />
-
- {t('Edit CSS')}}
- initialCss={this.state.css}
- templates={this.state.cssTemplates}
- onChange={this.changeCss}
- />
- >
- )}
-
- {!editMode && (
-
- )}
-
- {!editMode && (
-
- )}
+ {t('Refresh dashboard')}
+
+
+
+ {t('Set auto-refresh interval')}}
+ />
+
+
+ {editMode && (
+
+
+
+ )}
+
+ {editMode && (
+
+ {t('Edit dashboard properties')}
+
+ )}
+
+ {editMode && (
+
+ {t('Edit CSS')}}
+ initialCss={this.state.css}
+ templates={this.state.cssTemplates}
+ onChange={this.changeCss}
+ />
+
+ )}
+
+ {!editMode && (
+
+ {t('Download as image')}
+
+ )}
+
+ {!editMode && (
+
+ {t('Toggle FullScreen')}
+
+ )}
+
);
}
diff --git a/superset-frontend/src/dashboard/components/RefreshIntervalModal.jsx b/superset-frontend/src/dashboard/components/RefreshIntervalModal.jsx
index 2710e66aa3..492444502f 100644
--- a/superset-frontend/src/dashboard/components/RefreshIntervalModal.jsx
+++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.jsx
@@ -94,7 +94,6 @@ class RefreshIntervalModal extends React.PureComponent {
diff --git a/superset-frontend/src/dashboard/components/SaveModal.jsx b/superset-frontend/src/dashboard/components/SaveModal.jsx
index e3a85b5bbe..46c4c55e2c 100644
--- a/superset-frontend/src/dashboard/components/SaveModal.jsx
+++ b/superset-frontend/src/dashboard/components/SaveModal.jsx
@@ -41,14 +41,12 @@ const propTypes = {
colorNamespace: PropTypes.string,
colorScheme: PropTypes.string,
onSave: PropTypes.func.isRequired,
- isMenuItem: PropTypes.bool,
canOverwrite: PropTypes.bool.isRequired,
refreshFrequency: PropTypes.number.isRequired,
lastModifiedTime: PropTypes.number.isRequired,
};
const defaultProps = {
- isMenuItem: false,
saveType: SAVE_TYPE_OVERWRITE,
colorNamespace: undefined,
colorScheme: undefined,
@@ -157,7 +155,6 @@ class SaveModal extends React.PureComponent {
return (
(
-
+
));
const timeFrames = COMMON_TIME_FRAMES.map(timeFrame => {
const nextState = getStateFromCommonTimeFrame(timeFrame);
@@ -425,6 +422,7 @@ class DateFilterControl extends React.Component {
);
});
+
return (
-
-
+
+
+
-
this.setCustomRange('num', event.target.value)
}
onFocus={this.setTypeCustomRange}
- onKeyPress={this.onEnter}
+ onPressEnter={this.close}
value={this.state.num}
- style={{ height: '30px' }}
/>
-
-
+
+