mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
refactor: Replace react-bootstrap MenuItems with Antd Menu (#11554)
* Refactor SliceHeaderControls * Refactor DisplayQueryButton * Fix duplicate keys * Refactor SliceAdder * Move css from styles to Emotion * Fix e2e test
This commit is contained in:
parent
091432ea8e
commit
1490f3074d
@ -81,8 +81,7 @@ describe('Dashboard top-level controls', () => {
|
|||||||
.next()
|
.next()
|
||||||
.find('[data-test="dashboard-slice-refresh-tooltip"]')
|
.find('[data-test="dashboard-slice-refresh-tooltip"]')
|
||||||
.parent()
|
.parent()
|
||||||
.parent()
|
.should('have.class', 'ant-menu-item-disabled');
|
||||||
.should('have.class', 'disabled');
|
|
||||||
|
|
||||||
cy.wait(`@postJson_${mapId}_force`);
|
cy.wait(`@postJson_${mapId}_force`);
|
||||||
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
|
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
|
||||||
|
@ -18,10 +18,10 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||||
|
import { Menu } from 'src/common/components';
|
||||||
import ModalTrigger from 'src/components/ModalTrigger';
|
import ModalTrigger from 'src/components/ModalTrigger';
|
||||||
import { DisplayQueryButton } from 'src/explore/components/DisplayQueryButton';
|
import { DisplayQueryButton } from 'src/explore/components/DisplayQueryButton';
|
||||||
import { MenuItem } from 'react-bootstrap';
|
|
||||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
|
||||||
|
|
||||||
describe('DisplayQueryButton', () => {
|
describe('DisplayQueryButton', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
@ -51,6 +51,6 @@ describe('DisplayQueryButton', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find(ModalTrigger)).toHaveLength(3);
|
expect(wrapper.find(ModalTrigger)).toHaveLength(3);
|
||||||
expect(wrapper.find(MenuItem)).toHaveLength(5);
|
expect(wrapper.find(Menu.Item)).toHaveLength(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -191,7 +191,7 @@ export function Menu({
|
|||||||
</DropdownMenu.ItemGroup>,
|
</DropdownMenu.ItemGroup>,
|
||||||
]}
|
]}
|
||||||
{(navbarRight.version_string || navbarRight.version_sha) && [
|
{(navbarRight.version_string || navbarRight.version_sha) && [
|
||||||
<DropdownMenu.Divider key="user-divider" />,
|
<DropdownMenu.Divider key="navbar-divider" />,
|
||||||
<DropdownMenu.ItemGroup
|
<DropdownMenu.ItemGroup
|
||||||
key="about-section"
|
key="about-section"
|
||||||
title={t('About')}
|
title={t('About')}
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { MenuItem } from 'react-bootstrap';
|
|
||||||
import Modal from 'src/common/components/Modal';
|
import Modal from 'src/common/components/Modal';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
|
|
||||||
@ -31,7 +30,6 @@ const propTypes = {
|
|||||||
beforeOpen: PropTypes.func,
|
beforeOpen: PropTypes.func,
|
||||||
onExit: PropTypes.func,
|
onExit: PropTypes.func,
|
||||||
isButton: PropTypes.bool,
|
isButton: PropTypes.bool,
|
||||||
isMenuItem: PropTypes.bool,
|
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
width: PropTypes.string,
|
width: PropTypes.string,
|
||||||
@ -43,7 +41,6 @@ const defaultProps = {
|
|||||||
beforeOpen: () => {},
|
beforeOpen: () => {},
|
||||||
onExit: () => {},
|
onExit: () => {},
|
||||||
isButton: false,
|
isButton: false,
|
||||||
isMenuItem: false,
|
|
||||||
className: '',
|
className: '',
|
||||||
modalTitle: '',
|
modalTitle: '',
|
||||||
};
|
};
|
||||||
@ -102,14 +99,6 @@ export default class ModalTrigger extends React.Component {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.props.isMenuItem) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MenuItem onClick={this.open}>{this.props.triggerNode}</MenuItem>
|
|
||||||
{this.renderModal()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -29,7 +29,6 @@ const propTypes = {
|
|||||||
emailSubject: PropTypes.string,
|
emailSubject: PropTypes.string,
|
||||||
emailContent: PropTypes.string,
|
emailContent: PropTypes.string,
|
||||||
addDangerToast: PropTypes.func.isRequired,
|
addDangerToast: PropTypes.func.isRequired,
|
||||||
isMenuItem: PropTypes.bool,
|
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
triggerNode: PropTypes.node.isRequired,
|
triggerNode: PropTypes.node.isRequired,
|
||||||
};
|
};
|
||||||
@ -65,7 +64,6 @@ class URLShortLinkModal extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<ModalTrigger
|
<ModalTrigger
|
||||||
ref={this.setModalRef}
|
ref={this.setModalRef}
|
||||||
isMenuItem={this.props.isMenuItem}
|
|
||||||
triggerNode={this.props.triggerNode}
|
triggerNode={this.props.triggerNode}
|
||||||
beforeOpen={this.getCopyUrl}
|
beforeOpen={this.getCopyUrl}
|
||||||
modalTitle={this.props.title || t('Share Dashboard')}
|
modalTitle={this.props.title || t('Share Dashboard')}
|
||||||
|
@ -19,11 +19,11 @@
|
|||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { DropdownButton, MenuItem } from 'react-bootstrap';
|
|
||||||
import { List } from 'react-virtualized';
|
import { List } from 'react-virtualized';
|
||||||
import SearchInput, { createFilter } from 'react-search-input';
|
import SearchInput, { createFilter } from 'react-search-input';
|
||||||
import { t } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
|
|
||||||
|
import { Select } from 'src/common/components';
|
||||||
import AddSliceCard from './AddSliceCard';
|
import AddSliceCard from './AddSliceCard';
|
||||||
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
||||||
import DragDroppable from './dnd/DragDroppable';
|
import DragDroppable from './dnd/DragDroppable';
|
||||||
@ -52,12 +52,14 @@ const defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
|
const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
|
||||||
const KEYS_TO_SORT = [
|
const KEYS_TO_SORT = {
|
||||||
{ key: 'slice_name', label: 'Name' },
|
slice_name: 'Name',
|
||||||
{ key: 'viz_type', label: 'Vis type' },
|
viz_type: 'Vis type',
|
||||||
{ key: 'datasource_name', label: 'Dataset' },
|
datasource_name: 'Dataset',
|
||||||
{ key: 'changed_on', label: 'Recent' },
|
changed_on: 'Recent',
|
||||||
];
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SORT_KEY = 'changed_on';
|
||||||
|
|
||||||
const MARGIN_BOTTOM = 16;
|
const MARGIN_BOTTOM = 16;
|
||||||
const SIDEPANE_HEADER_HEIGHT = 30;
|
const SIDEPANE_HEADER_HEIGHT = 30;
|
||||||
@ -84,7 +86,7 @@ class SliceAdder extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
filteredSlices: [],
|
filteredSlices: [],
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
sortBy: KEYS_TO_SORT.findIndex(item => item.key === 'changed_on'),
|
sortBy: DEFAULT_SORT_KEY,
|
||||||
selectedSliceIdsSet: new Set(props.selectedSliceIds),
|
selectedSliceIdsSet: new Set(props.selectedSliceIds),
|
||||||
};
|
};
|
||||||
this.rowRenderer = this.rowRenderer.bind(this);
|
this.rowRenderer = this.rowRenderer.bind(this);
|
||||||
@ -102,7 +104,7 @@ class SliceAdder extends React.Component {
|
|||||||
if (nextProps.lastUpdated !== this.props.lastUpdated) {
|
if (nextProps.lastUpdated !== this.props.lastUpdated) {
|
||||||
nextState.filteredSlices = Object.values(nextProps.slices)
|
nextState.filteredSlices = Object.values(nextProps.slices)
|
||||||
.filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
|
.filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
|
||||||
.sort(SliceAdder.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key));
|
.sort(SliceAdder.sortByComparator(this.state.sortBy));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextProps.selectedSliceIds !== this.props.selectedSliceIds) {
|
if (nextProps.selectedSliceIds !== this.props.selectedSliceIds) {
|
||||||
@ -123,7 +125,7 @@ class SliceAdder extends React.Component {
|
|||||||
getFilteredSortedSlices(searchTerm, sortBy) {
|
getFilteredSortedSlices(searchTerm, sortBy) {
|
||||||
return Object.values(this.props.slices)
|
return Object.values(this.props.slices)
|
||||||
.filter(createFilter(searchTerm, KEYS_TO_FILTERS))
|
.filter(createFilter(searchTerm, KEYS_TO_FILTERS))
|
||||||
.sort(SliceAdder.sortByComparator(KEYS_TO_SORT[sortBy].key));
|
.sort(SliceAdder.sortByComparator(sortBy));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress(ev) {
|
handleKeyPress(ev) {
|
||||||
@ -216,17 +218,17 @@ class SliceAdder extends React.Component {
|
|||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
data-test="dashboard-charts-filter-search-input"
|
data-test="dashboard-charts-filter-search-input"
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<Select
|
||||||
title={`Sort by ${KEYS_TO_SORT[this.state.sortBy].label}`}
|
|
||||||
onSelect={this.handleSelect}
|
|
||||||
id="slice-adder-sortby"
|
id="slice-adder-sortby"
|
||||||
|
defaultValue={DEFAULT_SORT_KEY}
|
||||||
|
onChange={this.handleSelect}
|
||||||
>
|
>
|
||||||
{KEYS_TO_SORT.map((item, index) => (
|
{Object.entries(KEYS_TO_SORT).map(([key, label]) => (
|
||||||
<MenuItem key={item.key} eventKey={index}>
|
<Select.Option key={key} value={key}>
|
||||||
Sort by {item.label}
|
Sort by {label}
|
||||||
</MenuItem>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</DropdownButton>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{this.props.isLoading && <Loading />}
|
{this.props.isLoading && <Loading />}
|
||||||
{!this.props.isLoading && this.state.filteredSlices.length > 0 && (
|
{!this.props.isLoading && this.state.filteredSlices.length > 0 && (
|
||||||
|
@ -19,8 +19,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Dropdown, MenuItem } from 'react-bootstrap';
|
import { DropdownButton } from 'react-bootstrap';
|
||||||
import { t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
|
import { Menu } from 'src/common/components';
|
||||||
import URLShortLinkModal from '../../components/URLShortLinkModal';
|
import URLShortLinkModal from '../../components/URLShortLinkModal';
|
||||||
import downloadAsImage from '../../utils/downloadAsImage';
|
import downloadAsImage from '../../utils/downloadAsImage';
|
||||||
import getDashboardUrl from '../util/getDashboardUrl';
|
import getDashboardUrl from '../util/getDashboardUrl';
|
||||||
@ -58,41 +59,49 @@ const defaultProps = {
|
|||||||
sliceCanEdit: false,
|
sliceCanEdit: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MENU_KEYS = {
|
||||||
|
FORCE_REFRESH: 'force_refresh',
|
||||||
|
TOGGLE_CHART_DESCRIPTION: 'toggle_chart_description',
|
||||||
|
EXPLORE_CHART: 'explore_chart',
|
||||||
|
EXPORT_CSV: 'export_csv',
|
||||||
|
RESIZE_LABEL: 'resize_label',
|
||||||
|
SHARE_CHART: 'share_chart',
|
||||||
|
DOWNLOAD_AS_IMAGE: 'download_as_image',
|
||||||
|
};
|
||||||
|
|
||||||
|
const VerticalDotsContainer = styled.div`
|
||||||
|
padding: ${({ theme }) => theme.gridUnit / 4}px
|
||||||
|
${({ theme }) => theme.gridUnit * 1.5}px;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const VerticalDotsTrigger = () => (
|
const VerticalDotsTrigger = () => (
|
||||||
<div className="vertical-dots-container">
|
<VerticalDotsContainer>
|
||||||
<span className="dot" />
|
<span className="dot" />
|
||||||
<span className="dot" />
|
<span className="dot" />
|
||||||
<span className="dot" />
|
<span className="dot" />
|
||||||
</div>
|
</VerticalDotsContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
class SliceHeaderControls extends React.PureComponent {
|
class SliceHeaderControls extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.exportCSV = this.exportCSV.bind(this);
|
|
||||||
this.exploreChart = this.exploreChart.bind(this);
|
|
||||||
this.toggleControls = this.toggleControls.bind(this);
|
this.toggleControls = this.toggleControls.bind(this);
|
||||||
this.refreshChart = this.refreshChart.bind(this);
|
this.refreshChart = this.refreshChart.bind(this);
|
||||||
this.toggleExpandSlice = this.props.toggleExpandSlice.bind(
|
this.handleMenuClick = this.handleMenuClick.bind(this);
|
||||||
this,
|
|
||||||
this.props.slice.slice_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.handleToggleFullSize = this.handleToggleFullSize.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showControls: false,
|
showControls: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
exportCSV() {
|
|
||||||
this.props.exportCSV(this.props.slice.slice_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
exploreChart() {
|
|
||||||
this.props.exploreChart(this.props.slice.slice_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshChart() {
|
refreshChart() {
|
||||||
if (this.props.updatedDttm) {
|
if (this.props.updatedDttm) {
|
||||||
this.props.forceRefresh(
|
this.props.forceRefresh(
|
||||||
@ -108,8 +117,32 @@ class SliceHeaderControls extends React.PureComponent {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleToggleFullSize() {
|
handleMenuClick({ key, domEvent }) {
|
||||||
this.props.handleToggleFullSize();
|
switch (key) {
|
||||||
|
case MENU_KEYS.FORCE_REFRESH:
|
||||||
|
this.refreshChart();
|
||||||
|
break;
|
||||||
|
case MENU_KEYS.TOGGLE_CHART_DESCRIPTION:
|
||||||
|
this.props.toggleExpandSlice(this.props.slice.slice_id);
|
||||||
|
break;
|
||||||
|
case MENU_KEYS.EXPLORE_CHART:
|
||||||
|
this.props.exploreChart(this.props.slice.slice_id);
|
||||||
|
break;
|
||||||
|
case MENU_KEYS.EXPORT_CSV:
|
||||||
|
this.props.exportCSV(this.props.slice.slice_id);
|
||||||
|
break;
|
||||||
|
case MENU_KEYS.RESIZE_LABEL:
|
||||||
|
this.props.handleToggleFullSize();
|
||||||
|
break;
|
||||||
|
case MENU_KEYS.DOWNLOAD_AS_IMAGE:
|
||||||
|
downloadAsImage(
|
||||||
|
'.dashboard-component-chart-holder',
|
||||||
|
this.props.slice.slice_name,
|
||||||
|
)(domEvent);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -129,21 +162,21 @@ class SliceHeaderControls extends React.PureComponent {
|
|||||||
: (updatedWhen && t('Fetched %s', updatedWhen)) || '';
|
: (updatedWhen && t('Fetched %s', updatedWhen)) || '';
|
||||||
const resizeLabel = isFullSize ? t('Minimize') : t('Maximize');
|
const resizeLabel = isFullSize ? t('Minimize') : t('Maximize');
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<DropdownButton
|
||||||
id={`slice_${slice.slice_id}-controls`}
|
id={`slice_${slice.slice_id}-controls`}
|
||||||
pullRight
|
pullRight
|
||||||
|
noCaret
|
||||||
|
title={<VerticalDotsTrigger />}
|
||||||
|
style={{ padding: 0 }}
|
||||||
// react-bootstrap handles visibility, but call toggle to force a re-render
|
// react-bootstrap handles visibility, but call toggle to force a re-render
|
||||||
// and update the fetched/cached timestamps
|
// and update the fetched/cached timestamps
|
||||||
onToggle={this.toggleControls}
|
onToggle={this.toggleControls}
|
||||||
>
|
>
|
||||||
<Dropdown.Toggle className="slice-header-controls-trigger" noCaret>
|
<Menu onClick={this.handleMenuClick} selectable={false}>
|
||||||
<VerticalDotsTrigger />
|
<Menu.Item
|
||||||
</Dropdown.Toggle>
|
key={MENU_KEYS.FORCE_REFRESH}
|
||||||
|
|
||||||
<Dropdown.Menu>
|
|
||||||
<MenuItem
|
|
||||||
onClick={this.refreshChart}
|
|
||||||
disabled={this.props.chartStatus === 'loading'}
|
disabled={this.props.chartStatus === 'loading'}
|
||||||
|
style={{ height: 'auto', lineHeight: 'initial' }}
|
||||||
>
|
>
|
||||||
{t('Force refresh')}
|
{t('Force refresh')}
|
||||||
<div
|
<div
|
||||||
@ -152,50 +185,46 @@ class SliceHeaderControls extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
{refreshTooltip}
|
{refreshTooltip}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</Menu.Item>
|
||||||
|
|
||||||
<MenuItem divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
{slice.description && (
|
{slice.description && (
|
||||||
<MenuItem onClick={this.toggleExpandSlice}>
|
<Menu.Item key={MENU_KEYS.TOGGLE_CHART_DESCRIPTION}>
|
||||||
{t('Toggle chart description')}
|
{t('Toggle chart description')}
|
||||||
</MenuItem>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.props.supersetCanExplore && (
|
{this.props.supersetCanExplore && (
|
||||||
<MenuItem onClick={this.exploreChart}>
|
<Menu.Item key={MENU_KEYS.EXPLORE_CHART}>
|
||||||
{t('Explore chart')}
|
{t('Explore chart')}
|
||||||
</MenuItem>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.props.supersetCanCSV && (
|
{this.props.supersetCanCSV && (
|
||||||
<MenuItem onClick={this.exportCSV}>{t('Export CSV')}</MenuItem>
|
<Menu.Item key={MENU_KEYS.EXPORT_CSV}>{t('Export CSV')}</Menu.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MenuItem onClick={this.handleToggleFullSize}>{resizeLabel}</MenuItem>
|
<Menu.Item key={MENU_KEYS.RESIZE_LABEL}>{resizeLabel}</Menu.Item>
|
||||||
|
|
||||||
<URLShortLinkModal
|
<Menu.Item key={MENU_KEYS.SHARE_CHART}>
|
||||||
url={getDashboardUrl(
|
<URLShortLinkModal
|
||||||
window.location.pathname,
|
url={getDashboardUrl(
|
||||||
getActiveFilters(),
|
window.location.pathname,
|
||||||
componentId,
|
getActiveFilters(),
|
||||||
)}
|
componentId,
|
||||||
addDangerToast={addDangerToast}
|
)}
|
||||||
isMenuItem
|
addDangerToast={addDangerToast}
|
||||||
title={t('Share chart')}
|
title={t('Share chart')}
|
||||||
triggerNode={<span>{t('Share chart')}</span>}
|
triggerNode={<span>{t('Share chart')}</span>}
|
||||||
/>
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<MenuItem
|
<Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
|
||||||
onClick={downloadAsImage(
|
|
||||||
'.dashboard-component-chart-holder',
|
|
||||||
slice.slice_name,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t('Download as image')}
|
{t('Download as image')}
|
||||||
</MenuItem>
|
</Menu.Item>
|
||||||
</Dropdown.Menu>
|
</Menu>
|
||||||
</Dropdown>
|
</DropdownButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,14 +112,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slice-header-controls-trigger {
|
|
||||||
padding: 2px 6px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
@dot-diameter: 4px;
|
@dot-diameter: 4px;
|
||||||
|
|
||||||
@ -131,10 +123,6 @@
|
|||||||
background-color: @gray;
|
background-color: @gray;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
.vertical-dots-container & {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
a[role='menuitem'] & {
|
a[role='menuitem'] & {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
@ -138,16 +138,8 @@ body {
|
|||||||
padding: 9px 0;
|
padding: 9px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu li a {
|
.dropdown-menu li {
|
||||||
padding: 3px 16px;
|
font-weight: @font-weight-normal;
|
||||||
color: @almost-black;
|
|
||||||
font-size: @font-size-m;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background: @menu-hover;
|
|
||||||
color: @almost-black;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,16 +26,11 @@ import markdownSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/mar
|
|||||||
import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
|
import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
|
||||||
import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
|
import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
|
||||||
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
|
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
|
||||||
import {
|
import { DropdownButton, Row, Col, FormControl } from 'react-bootstrap';
|
||||||
DropdownButton,
|
|
||||||
MenuItem,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
FormControl,
|
|
||||||
} from 'react-bootstrap';
|
|
||||||
import { Table } from 'reactable-arc';
|
import { Table } from 'reactable-arc';
|
||||||
import { t } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
|
|
||||||
|
import { Menu } from 'src/common/components';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import getClientErrorObject from '../../utils/getClientErrorObject';
|
import getClientErrorObject from '../../utils/getClientErrorObject';
|
||||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||||
@ -65,6 +60,12 @@ const propTypes = {
|
|||||||
slice: PropTypes.object,
|
slice: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MENU_KEYS = {
|
||||||
|
EDIT_PROPERTIES: 'edit_properties',
|
||||||
|
RUN_IN_SQL_LAB: 'run_in_sql_lab',
|
||||||
|
DOWNLOAD_AS_IMAGE: 'download_as_image',
|
||||||
|
};
|
||||||
|
|
||||||
export class DisplayQueryButton extends React.PureComponent {
|
export class DisplayQueryButton extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -81,8 +82,8 @@ export class DisplayQueryButton extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
this.beforeOpen = this.beforeOpen.bind(this);
|
this.beforeOpen = this.beforeOpen.bind(this);
|
||||||
this.changeFilterText = this.changeFilterText.bind(this);
|
this.changeFilterText = this.changeFilterText.bind(this);
|
||||||
this.openPropertiesModal = this.openPropertiesModal.bind(this);
|
|
||||||
this.closePropertiesModal = this.closePropertiesModal.bind(this);
|
this.closePropertiesModal = this.closePropertiesModal.bind(this);
|
||||||
|
this.handleMenuClick = this.handleMenuClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeOpen(resultType) {
|
beforeOpen(resultType) {
|
||||||
@ -118,10 +119,6 @@ export class DisplayQueryButton extends React.PureComponent {
|
|||||||
this.setState({ filterText: event.target.value });
|
this.setState({ filterText: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectSQLLab() {
|
|
||||||
this.props.onOpenInEditor(this.props.latestQueryFormData);
|
|
||||||
}
|
|
||||||
|
|
||||||
openPropertiesModal() {
|
openPropertiesModal() {
|
||||||
this.setState({ isPropertiesModalOpen: true });
|
this.setState({ isPropertiesModalOpen: true });
|
||||||
}
|
}
|
||||||
@ -130,6 +127,35 @@ export class DisplayQueryButton extends React.PureComponent {
|
|||||||
this.setState({ isPropertiesModalOpen: false });
|
this.setState({ isPropertiesModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMenuClick({ key, domEvent }) {
|
||||||
|
const {
|
||||||
|
chartHeight,
|
||||||
|
slice,
|
||||||
|
onOpenInEditor,
|
||||||
|
latestQueryFormData,
|
||||||
|
} = this.props;
|
||||||
|
switch (key) {
|
||||||
|
case MENU_KEYS.EDIT_PROPERTIES:
|
||||||
|
this.openPropertiesModal();
|
||||||
|
break;
|
||||||
|
case MENU_KEYS.RUN_IN_SQL_LAB:
|
||||||
|
onOpenInEditor(latestQueryFormData);
|
||||||
|
break;
|
||||||
|
case MENU_KEYS.DOWNLOAD_AS_IMAGE:
|
||||||
|
downloadAsImage(
|
||||||
|
'.chart-container',
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
slice?.slice_name ?? t('New chart'),
|
||||||
|
{
|
||||||
|
height: parseInt(chartHeight, 10),
|
||||||
|
},
|
||||||
|
)(domEvent);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderQueryModalBody() {
|
renderQueryModalBody() {
|
||||||
if (this.state.isLoading) {
|
if (this.state.isLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
@ -230,7 +256,7 @@ export class DisplayQueryButton extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { chartHeight, slice } = this.props;
|
const { slice } = this.props;
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
noCaret
|
noCaret
|
||||||
@ -245,62 +271,56 @@ export class DisplayQueryButton extends React.PureComponent {
|
|||||||
pullRight
|
pullRight
|
||||||
id="query"
|
id="query"
|
||||||
>
|
>
|
||||||
{slice && (
|
<Menu onClick={this.handleMenuClick} selectable={false}>
|
||||||
<>
|
{slice && [
|
||||||
<MenuItem onClick={this.openPropertiesModal}>
|
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
||||||
{t('Edit properties')}
|
{t('Edit properties')}
|
||||||
</MenuItem>
|
</Menu.Item>,
|
||||||
<PropertiesModal
|
<PropertiesModal
|
||||||
slice={slice}
|
slice={slice}
|
||||||
show={this.state.isPropertiesModalOpen}
|
show={this.state.isPropertiesModalOpen}
|
||||||
onHide={this.closePropertiesModal}
|
onHide={this.closePropertiesModal}
|
||||||
onSave={this.props.sliceUpdated}
|
onSave={this.props.sliceUpdated}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
<Menu.Item>
|
||||||
|
<ModalTrigger
|
||||||
|
triggerNode={
|
||||||
|
<span data-test="view-query-menu-item">{t('View query')}</span>
|
||||||
|
}
|
||||||
|
modalTitle={t('View query')}
|
||||||
|
beforeOpen={() => this.beforeOpen('query')}
|
||||||
|
modalBody={this.renderQueryModalBody()}
|
||||||
|
responsive
|
||||||
/>
|
/>
|
||||||
</>
|
</Menu.Item>
|
||||||
)}
|
<Menu.Item>
|
||||||
<ModalTrigger
|
<ModalTrigger
|
||||||
isMenuItem
|
triggerNode={<span>{t('View results')}</span>}
|
||||||
triggerNode={
|
modalTitle={t('View results')}
|
||||||
<span data-test="view-query-menu-item">{t('View query')}</span>
|
beforeOpen={() => this.beforeOpen('results')}
|
||||||
}
|
modalBody={this.renderResultsModalBody()}
|
||||||
modalTitle={t('View query')}
|
responsive
|
||||||
beforeOpen={() => this.beforeOpen('query')}
|
/>
|
||||||
modalBody={this.renderQueryModalBody()}
|
</Menu.Item>
|
||||||
responsive
|
<Menu.Item>
|
||||||
/>
|
<ModalTrigger
|
||||||
<ModalTrigger
|
triggerNode={<span>{t('View samples')}</span>}
|
||||||
isMenuItem
|
modalTitle={t('View samples')}
|
||||||
triggerNode={<span>{t('View results')}</span>}
|
beforeOpen={() => this.beforeOpen('samples')}
|
||||||
modalTitle={t('View results')}
|
modalBody={this.renderSamplesModalBody()}
|
||||||
beforeOpen={() => this.beforeOpen('results')}
|
responsive
|
||||||
modalBody={this.renderResultsModalBody()}
|
/>
|
||||||
responsive
|
</Menu.Item>
|
||||||
/>
|
{this.state.sqlSupported && (
|
||||||
<ModalTrigger
|
<Menu.Item key={MENU_KEYS.RUN_IN_SQL_LAB}>
|
||||||
isMenuItem
|
{t('Run in SQL Lab')}
|
||||||
triggerNode={<span>{t('View samples')}</span>}
|
</Menu.Item>
|
||||||
modalTitle={t('View samples')}
|
|
||||||
beforeOpen={() => this.beforeOpen('samples')}
|
|
||||||
modalBody={this.renderSamplesModalBody()}
|
|
||||||
responsive
|
|
||||||
/>
|
|
||||||
{this.state.sqlSupported && (
|
|
||||||
<MenuItem eventKey="3" onClick={this.redirectSQLLab.bind(this)}>
|
|
||||||
{t('Run in SQL Lab')}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem
|
|
||||||
onClick={downloadAsImage(
|
|
||||||
'.chart-container',
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
slice?.slice_name ?? t('New chart'),
|
|
||||||
{
|
|
||||||
height: parseInt(chartHeight, 10),
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
>
|
<Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
|
||||||
{t('Download as image')}
|
{t('Download as image')}
|
||||||
</MenuItem>
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -88,8 +88,6 @@ const FREEFORM_TOOLTIP = t(
|
|||||||
'`2 weeks from now` can be used.',
|
'`2 weeks from now` can be used.',
|
||||||
);
|
);
|
||||||
|
|
||||||
const DATE_FILTER_POPOVER_STYLE = { width: '250px' };
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
animation: PropTypes.bool,
|
animation: PropTypes.bool,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
@ -173,12 +171,34 @@ function getStateFromCustomRange(value) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const Styles = styled.div`
|
const TimeFramesStyles = styled.div`
|
||||||
.radio {
|
.radio {
|
||||||
margin: ${({ theme }) => theme.gridUnit}px 0;
|
margin: ${({ theme }) => theme.gridUnit}px 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const PopoverContentStyles = styled.div`
|
||||||
|
width: ${({ theme }) => theme.gridUnit * 60}px;
|
||||||
|
|
||||||
|
.timeframes-container {
|
||||||
|
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative-timerange-container {
|
||||||
|
display: flex;
|
||||||
|
margin-top: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timerange-input {
|
||||||
|
width: ${({ theme }) => theme.gridUnit * 15}px;
|
||||||
|
margin: 0 ${({ theme }) => theme.gridUnit}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime {
|
||||||
|
margin: ${({ theme }) => theme.gridUnit}px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
class DateFilterControl extends React.Component {
|
class DateFilterControl extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -399,7 +419,7 @@ class DateFilterControl extends React.Component {
|
|||||||
const timeRange = buildTimeRangeString(nextState.since, nextState.until);
|
const timeRange = buildTimeRangeString(nextState.since, nextState.until);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styles key={timeFrame}>
|
<TimeFramesStyles key={timeFrame}>
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
key={timeFrame}
|
key={timeFrame}
|
||||||
placement="right"
|
placement="right"
|
||||||
@ -419,14 +439,13 @@ class DateFilterControl extends React.Component {
|
|||||||
</Radio>
|
</Radio>
|
||||||
</div>
|
</div>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
</Styles>
|
</TimeFramesStyles>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PopoverContentStyles
|
||||||
id="filter-popover"
|
id="filter-popover"
|
||||||
style={DATE_FILTER_POPOVER_STYLE}
|
|
||||||
ref={ref => {
|
ref={ref => {
|
||||||
this.popoverContainer = ref;
|
this.popoverContainer = ref;
|
||||||
}}
|
}}
|
||||||
@ -438,7 +457,7 @@ class DateFilterControl extends React.Component {
|
|||||||
onSelect={this.changeTab}
|
onSelect={this.changeTab}
|
||||||
>
|
>
|
||||||
<Tabs.TabPane key="1" tab="Defaults" forceRender>
|
<Tabs.TabPane key="1" tab="Defaults" forceRender>
|
||||||
<div style={{ marginLeft: '8px' }}>
|
<div className="timeframes-container">
|
||||||
<FormGroup>{timeFrames}</FormGroup>
|
<FormGroup>{timeFrames}</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
@ -449,48 +468,37 @@ class DateFilterControl extends React.Component {
|
|||||||
isSelected={this.state.type === TYPES.CUSTOM_RANGE}
|
isSelected={this.state.type === TYPES.CUSTOM_RANGE}
|
||||||
onSelect={this.setTypeCustomRange}
|
onSelect={this.setTypeCustomRange}
|
||||||
>
|
>
|
||||||
<div
|
<div className="relative-timerange-container clearfix centered">
|
||||||
className="clearfix centered"
|
<Select
|
||||||
style={{ marginTop: '8px', display: 'flex' }}
|
value={this.state.rel}
|
||||||
>
|
onSelect={value => this.setCustomRange('rel', value)}
|
||||||
<div className="input-inline">
|
onFocus={this.setTypeCustomRange}
|
||||||
<Select
|
|
||||||
value={this.state.rel}
|
|
||||||
onSelect={value => this.setCustomRange('rel', value)}
|
|
||||||
onFocus={this.setTypeCustomRange}
|
|
||||||
>
|
|
||||||
<Select.Option value={RELATIVE_TIME_OPTIONS.LAST}>
|
|
||||||
Last
|
|
||||||
</Select.Option>
|
|
||||||
<Select.Option value={RELATIVE_TIME_OPTIONS.NEXT}>
|
|
||||||
Next
|
|
||||||
</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{ width: '60px' }}
|
|
||||||
className="input-inline m-l-5 m-r-3"
|
|
||||||
>
|
>
|
||||||
<Input
|
<Select.Option value={RELATIVE_TIME_OPTIONS.LAST}>
|
||||||
type="text"
|
Last
|
||||||
onChange={event =>
|
</Select.Option>
|
||||||
this.setCustomRange('num', event.target.value)
|
<Select.Option value={RELATIVE_TIME_OPTIONS.NEXT}>
|
||||||
}
|
Next
|
||||||
onFocus={this.setTypeCustomRange}
|
</Select.Option>
|
||||||
onPressEnter={this.close}
|
</Select>
|
||||||
value={this.state.num}
|
<Input
|
||||||
/>
|
className="timerange-input"
|
||||||
</div>
|
type="text"
|
||||||
<div className="input-inline">
|
onChange={event =>
|
||||||
<Select
|
this.setCustomRange('num', event.target.value)
|
||||||
value={this.state.grain}
|
}
|
||||||
onFocus={this.setTypeCustomRange}
|
onFocus={this.setTypeCustomRange}
|
||||||
onSelect={value => this.setCustomRange('grain', value)}
|
onPressEnter={this.close}
|
||||||
dropdownMatchSelectWidth={false}
|
value={this.state.num}
|
||||||
>
|
/>
|
||||||
{grainOptions}
|
<Select
|
||||||
</Select>
|
value={this.state.grain}
|
||||||
</div>
|
onFocus={this.setTypeCustomRange}
|
||||||
|
onSelect={value => this.setCustomRange('grain', value)}
|
||||||
|
dropdownMatchSelectWidth={false}
|
||||||
|
>
|
||||||
|
{grainOptions}
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</PopoverSection>
|
</PopoverSection>
|
||||||
<PopoverSection
|
<PopoverSection
|
||||||
@ -505,48 +513,42 @@ class DateFilterControl extends React.Component {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<InputGroup data-test="date-input-group">
|
<InputGroup data-test="date-input-group">
|
||||||
<div style={{ margin: '5px 0' }}>
|
<Datetime
|
||||||
<Datetime
|
className="datetime"
|
||||||
inputProps={{ 'data-test': 'date-from-input' }}
|
inputProps={{ 'data-test': 'date-from-input' }}
|
||||||
value={this.state.since}
|
value={this.state.since}
|
||||||
defaultValue={this.state.since}
|
defaultValue={this.state.since}
|
||||||
viewDate={this.state.since}
|
viewDate={this.state.since}
|
||||||
onChange={value =>
|
onChange={value => this.setCustomStartEnd('since', value)}
|
||||||
this.setCustomStartEnd('since', value)
|
isValidDate={this.isValidSince}
|
||||||
}
|
onClick={this.setTypeCustomStartEnd}
|
||||||
isValidDate={this.isValidSince}
|
renderInput={props =>
|
||||||
onClick={this.setTypeCustomStartEnd}
|
this.renderInput(props, 'showSinceCalendar')
|
||||||
renderInput={props =>
|
}
|
||||||
this.renderInput(props, 'showSinceCalendar')
|
open={this.state.showSinceCalendar}
|
||||||
}
|
viewMode={this.state.sinceViewMode}
|
||||||
open={this.state.showSinceCalendar}
|
onViewModeChange={sinceViewMode =>
|
||||||
viewMode={this.state.sinceViewMode}
|
this.setState({ sinceViewMode })
|
||||||
onViewModeChange={sinceViewMode =>
|
}
|
||||||
this.setState({ sinceViewMode })
|
/>
|
||||||
}
|
<Datetime
|
||||||
/>
|
className="datetime"
|
||||||
</div>
|
inputProps={{ 'data-test': 'date-to-input' }}
|
||||||
<div style={{ margin: '5px 0' }}>
|
value={this.state.until}
|
||||||
<Datetime
|
defaultValue={this.state.until}
|
||||||
inputProps={{ 'data-test': 'date-to-input' }}
|
viewDate={this.state.until}
|
||||||
value={this.state.until}
|
onChange={value => this.setCustomStartEnd('until', value)}
|
||||||
defaultValue={this.state.until}
|
isValidDate={this.isValidUntil}
|
||||||
viewDate={this.state.until}
|
onClick={this.setTypeCustomStartEnd}
|
||||||
onChange={value =>
|
renderInput={props =>
|
||||||
this.setCustomStartEnd('until', value)
|
this.renderInput(props, 'showUntilCalendar')
|
||||||
}
|
}
|
||||||
isValidDate={this.isValidUntil}
|
open={this.state.showUntilCalendar}
|
||||||
onClick={this.setTypeCustomStartEnd}
|
viewMode={this.state.untilViewMode}
|
||||||
renderInput={props =>
|
onViewModeChange={untilViewMode =>
|
||||||
this.renderInput(props, 'showUntilCalendar')
|
this.setState({ untilViewMode })
|
||||||
}
|
}
|
||||||
open={this.state.showUntilCalendar}
|
/>
|
||||||
viewMode={this.state.untilViewMode}
|
|
||||||
onViewModeChange={untilViewMode =>
|
|
||||||
this.setState({ untilViewMode })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
</PopoverSection>
|
</PopoverSection>
|
||||||
@ -564,7 +566,7 @@ class DateFilterControl extends React.Component {
|
|||||||
Ok
|
Ok
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PopoverContentStyles>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user