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:
Kamil Gabryjelski 2020-11-05 22:30:03 +01:00 committed by GitHub
parent 091432ea8e
commit 1490f3074d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 289 additions and 270 deletions

View File

@ -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(

View File

@ -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);
}); });
}); });

View File

@ -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')}

View File

@ -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 (
<> <>

View File

@ -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')}

View File

@ -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 && (

View File

@ -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>
); );
} }
} }

View File

@ -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;

View File

@ -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;
}
} }
} }

View File

@ -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>
); );
} }

View File

@ -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>
); );
} }