diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton.tsx index f0c1e99e68e29..2297f8ff684d8 100644 --- a/superset-frontend/src/SqlLab/components/RunQueryActionButton.tsx +++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton.tsx @@ -17,75 +17,110 @@ * under the License. */ import React from 'react'; -import { t } from '@superset-ui/core'; +import { t, styled, supersetTheme } from '@superset-ui/core'; -import { Dropdown, Menu } from 'src/common/components'; +import { Menu } from 'src/common/components'; +import Button, { ButtonProps } from 'src/components/Button'; import Icon from 'src/components/Icon'; - -const NO_OP = () => undefined; +import { DropdownButton, DropdownProps } from 'src/common/components/Dropdown'; interface Props { allowAsync: boolean; - dbId?: number; queryState?: string; runQuery: (c?: boolean) => void; selectedText?: string; stopQuery: () => void; sql: string; - overlayCreateAsMenu: typeof Menu; + overlayCreateAsMenu: typeof Menu | null; } +type QueryButtonProps = DropdownProps | ButtonProps; + +const buildText = ( + shouldShowStopButton: boolean, + selectedText: string | undefined, +): string | JSX.Element => { + if (shouldShowStopButton) { + return ( + <> + {t('Stop')} + + ); + } + if (selectedText) { + return t('Run Selection'); + } + return t('Run'); +}; + +const onClick = ( + shouldShowStopButton: boolean, + allowAsync: boolean, + runQuery: (c?: boolean) => void = () => undefined, + stopQuery = () => {}, +): void => { + if (shouldShowStopButton) return stopQuery(); + if (allowAsync) { + return runQuery(true); + } + return runQuery(false); +}; + +const StyledButton = styled.span` + button { + line-height: 13px; + &:last-of-type { + margin-right: ${({ theme }) => theme.gridUnit * 2}px; + } + } +`; + const RunQueryActionButton = ({ allowAsync = false, - dbId, queryState, - runQuery = NO_OP, selectedText, - stopQuery = NO_OP, sql = '', overlayCreateAsMenu, + runQuery, + stopQuery, }: Props) => { - const runBtnText = selectedText ? t('Run Selection') : t('Run'); const btnStyle = selectedText ? 'warning' : 'primary'; const shouldShowStopBtn = !!queryState && ['running', 'pending'].indexOf(queryState) > -1; - if (shouldShowStopBtn) { - return ( - } - type={btnStyle} - overlay={overlayCreateAsMenu} - disabled={!sql.trim()} - > - {t('Stop')} - - ); - } - if (allowAsync) { - return ( - runQuery(true)} - icon={} - type={btnStyle} - overlay={overlayCreateAsMenu} + const ButtonComponent: React.FC = overlayCreateAsMenu + ? (DropdownButton as React.FC) + : Button; + + return ( + + + onClick(shouldShowStopBtn, allowAsync, runQuery, stopQuery) + } disabled={!sql.trim()} + buttonSize="small" + tooltip={ + shouldShowStopBtn + ? t('Stop running (Ctrl + x)') + : t('Run query (Ctrl + Return)') + } + cta + {...(overlayCreateAsMenu + ? { + overlay: overlayCreateAsMenu, + icon: ( + + ), + } + : { buttonStyle: btnStyle })} > - {runBtnText} - - ); - } - return ( - runQuery(false)} - icon={} - type={btnStyle} - overlay={overlayCreateAsMenu} - disabled={!sql.trim()} - > - {runBtnText} - + {buildText(shouldShowStopBtn, selectedText)} + + ); }; diff --git a/superset-frontend/src/SqlLab/components/SqlEditor.jsx b/superset-frontend/src/SqlLab/components/SqlEditor.jsx index 0f465ff612888..6ae49763660f9 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor.jsx @@ -25,7 +25,7 @@ import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; import { Form } from 'react-bootstrap'; import Split from 'react-split'; -import { t, styled } from '@superset-ui/core'; +import { t, styled, supersetTheme } from '@superset-ui/core'; import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; import StyledModal from 'src/common/components/Modal'; @@ -88,8 +88,46 @@ const WINDOW_RESIZE_THROTTLE_MS = 100; const LimitSelectStyled = styled.span` .ant-dropdown-trigger { + align-items: center; color: black; + display: flex; + font-size: 12px; + margin-right: ${({ theme }) => theme.gridUnit * 2}px; text-decoration: none; + span { + display: inline-block; + margin-right: ${({ theme }) => theme.gridUnit * 2}px; + &:last-of-type: { + margin-right: ${({ theme }) => theme.gridUnit * 4}px; + } + } + } +`; + +const StyledToolbar = styled.div` + padding: 10px 10px 5px 10px; + background-color: @lightest; + display: flex; + justify-content: space-between; + border: 1px solid ${supersetTheme.colors.grayscale.light2}; + border-top: 0; + + form { + margin-block-end: 0; + } + + .leftItems form, + .rightItems { + display: flex; + align-items: center; + & > span { + margin-right: ${({ theme }) => theme.gridUnit * 2}px; + display: inline-block; + + &:last-child { + margin-right: 0; + } + } } `; @@ -100,7 +138,7 @@ const propTypes = { tables: PropTypes.array.isRequired, editorQueries: PropTypes.array.isRequired, dataPreviewQueries: PropTypes.array.isRequired, - queryEditorId: PropTypes.number.isRequired, + queryEditorId: PropTypes.string.isRequired, hideLeftBar: PropTypes.bool, defaultQueryLimit: PropTypes.number.isRequired, maxRow: PropTypes.number.isRequired, @@ -534,10 +572,8 @@ class SqlEditor extends React.PureComponent { } // eslint-disable-next-line camelcase - const { - allow_ctas: allowCTAS, - allow_cvas: allowCVAS, - } = this.props.database; + const { allow_ctas: allowCTAS, allow_cvas: allowCVAS } = + this.props.database || {}; const showMenu = allowCTAS || allowCVAS; const runMenuBtn = ( @@ -572,7 +608,7 @@ class SqlEditor extends React.PureComponent { ); return ( -
+
@@ -582,13 +618,12 @@ class SqlEditor extends React.PureComponent { ? this.props.database.allow_run_async : false } - dbId={qe.dbId} queryState={this.props.latestQuery?.state} runQuery={this.runQuery} selectedText={qe.selectedText} stopQuery={this.stopQuery} sql={this.state.sql} - overlayCreateAsMenu={showMenu ? runMenuBtn : <>} + overlayCreateAsMenu={showMenu ? runMenuBtn : null} /> {isFeatureEnabled(FeatureFlag.ESTIMATE_QUERY_COST) && @@ -607,31 +642,35 @@ class SqlEditor extends React.PureComponent { )} {limitWarning} - {this.props.latestQuery && ( - - )} e.preventDefault()}> - LIMIT:{' '} - {this.props.queryEditor.queryLimit !== undefined - ? this.props.queryEditor.queryLimit - : this.props.defaultQueryLimit} + LIMIT: + + {( + this.props.queryEditor.queryLimit || + this.props.defaultQueryLimit + ).toLocaleString()} + + + {this.props.latestQuery && ( + + )}
{limitWarning} - {this.props.latestQuery && ( - - )}
-
+ ); } diff --git a/superset-frontend/src/SqlLab/main.less b/superset-frontend/src/SqlLab/main.less index 7123ed033f9fd..57be0412d7075 100644 --- a/superset-frontend/src/SqlLab/main.less +++ b/superset-frontend/src/SqlLab/main.less @@ -132,33 +132,6 @@ div.Workspace { padding-top: 10px; } -.sql-toolbar { - padding: 10px 10px 5px 10px; - background-color: @lightest; - display: flex; - justify-content: space-between; - border: 1px solid @gray-light; - border-top: 0; - - form { - margin-block-end: 0; - } - - .leftItems form, - .rightItems { - & > span { - margin-right: 5px; - margin-bottom: 5px; - display: inline-block; - - &:last-child { - margin-right: 0; - font-feature-settings: 'kern' 1, 'tnum' 1; - } - } - } -} - .no-shadow { box-shadow: none; background-color: transparent; diff --git a/superset-frontend/src/common/components/Dropdown.tsx b/superset-frontend/src/common/components/Dropdown.tsx index 47733faebfcf0..1b9e13cadbc87 100644 --- a/superset-frontend/src/common/components/Dropdown.tsx +++ b/superset-frontend/src/common/components/Dropdown.tsx @@ -17,8 +17,9 @@ * under the License. */ import React from 'react'; -import { Dropdown as AntdDropdown } from 'src/common/components'; +import { Dropdown as AntdDropdown, Tooltip } from 'src/common/components'; import { styled } from '@superset-ui/core'; +import kebabCase from 'lodash/kebabCase'; const MenuDots = styled.div` width: ${({ theme }) => theme.gridUnit * 0.75}px; @@ -65,8 +66,47 @@ const MenuDotsWrapper = styled.div` padding-left: ${({ theme }) => theme.gridUnit}px; `; -interface DropdownProps { +const StyledDropdownButton = styled.div` + .ant-btn-group { + button.ant-btn { + background-color: ${({ theme }) => theme.colors.primary.dark1}; + border-color: transparent; + color: ${({ theme }) => theme.colors.grayscale.light5}; + font-size: 12px; + line-height: 13px; + outline: none; + text-transform: uppercase; + &:first-of-type { + border-radius: ${({ theme }) => + `${theme.gridUnit}px 0 0 ${theme.gridUnit}px`}; + margin-right: 1px; + width: 108px; + } + &:last-of-type { + border-radius: ${({ theme }) => + `0 ${theme.gridUnit}px ${theme.gridUnit}px 0`}; + margin-right: ${({ theme }) => theme.gridUnit * 2}px; + width: ${({ theme }) => theme.gridUnit * 9}px; + &:before, + &:hover:before { + border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light5}; + content: ''; + display: block; + height: 23px; + margin: 0; + position: absolute; + top: ${({ theme }) => theme.gridUnit * 0.75}px; + width: ${({ theme }) => theme.gridUnit * 0.25}px; + } + } + } + } +`; + +export interface DropdownProps { overlay: React.ReactElement; + tooltip?: string; + placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; } export const Dropdown = ({ overlay, ...rest }: DropdownProps) => ( @@ -76,3 +116,28 @@ export const Dropdown = ({ overlay, ...rest }: DropdownProps) => ( ); + +export const DropdownButton = ({ + overlay, + tooltip, + placement, + ...rest +}: DropdownProps) => { + const button = ( + + + + ); + if (tooltip) { + return ( + + {button} + + ); + } + return { button }; +}; diff --git a/superset-frontend/src/common/components/index.tsx b/superset-frontend/src/common/components/index.tsx index 9d246f6b3274b..d613c8c0e2d7b 100644 --- a/superset-frontend/src/common/components/index.tsx +++ b/superset-frontend/src/common/components/index.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { styled } from '@superset-ui/core'; // eslint-disable-next-line no-restricted-imports import { Menu as AntdMenu, Dropdown, Skeleton } from 'antd'; -import { DropDownProps } from 'antd/lib/dropdown'; +import { DropDownProps as AntdDropdownProps } from 'antd/lib/dropdown'; /* Antd is re-exported from here so we can override components with Emotion as needed. @@ -78,7 +78,7 @@ export const Menu = Object.assign(AntdMenu, { Item: MenuItem, }); -export const NoAnimationDropdown = (props: DropDownProps) => ( +export const NoAnimationDropdown = (props: AntdDropdownProps) => (