Wrap query with limit within the web console (#7449)

* wrap with limit

* make actual menu checkbox component
This commit is contained in:
Vadim Ogievetsky 2019-04-12 08:51:37 -07:00 committed by Fangjin Yang
parent 3aae4aaf8b
commit 60dd75d3d9
6 changed files with 189 additions and 68 deletions

View File

@ -0,0 +1,26 @@
/*
* 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.
*/
.menu-checkbox {
height: 30px;
padding: 7px 4px;
label {
margin: 0;
}
}

View File

@ -0,0 +1,31 @@
/*
* 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 { Checkbox, ICheckboxProps } from '@blueprintjs/core';
import * as React from 'react';
import './menu-checkbox.scss';
export class MenuCheckbox extends React.Component<ICheckboxProps, {}> {
render() {
return <li className="menu-checkbox">
<Checkbox {...this.props}/>
</li>;
}
}

View File

@ -42,23 +42,6 @@
}
.sql-control-popover {
padding:10px;
min-width: 120px;
.bp3-form-group {
margin-bottom: 0;
}
button {
span {
position: relative;
left: -7px;
}
padding-right: 35px;
}
}
.ace_tooltip {
padding: 10px;
background-color: #333D47;

View File

@ -16,7 +16,15 @@
* limitations under the License.
*/
import { Button, Checkbox, Classes, FormGroup, Intent, Menu, Popover, Position } from '@blueprintjs/core';
import {
Button,
ButtonGroup,
Intent,
Menu,
MenuItem,
Popover,
Position
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import * as ace from 'brace';
@ -25,6 +33,7 @@ import 'brace/mode/hjson';
import 'brace/mode/sql';
import 'brace/theme/solarized_dark';
import * as classNames from 'classnames';
import * as Hjson from 'hjson';
import * as React from 'react';
import AceEditor from 'react-ace';
import * as ReactDOMServer from 'react-dom/server';
@ -32,21 +41,34 @@ import * as ReactDOMServer from 'react-dom/server';
import { SQLFunctionDoc } from '../../lib/sql-function-doc';
import { AppToaster } from '../singletons/toaster';
import { MenuCheckbox } from './menu-checkbox';
import './sql-control.scss';
function validHjson(query: string) {
try {
Hjson.parse(query);
return true;
} catch {
return false;
}
}
const langTools = ace.acequire('ace/ext/language_tools');
export interface SqlControlProps extends React.Props<any> {
initSql: string | null;
onRun: (query: string) => void;
onExplain: (query: string) => void;
onRun: (query: string, bypassCache: boolean, wrapQuery: boolean) => void;
onExplain: (sqlQuery: string) => void;
queryElapsed: number | null;
}
export interface SqlControlState {
query: string;
autoCompleteOn: boolean;
autoComplete: boolean;
autoCompleteLoading: boolean;
bypassCache: boolean;
wrapQuery: boolean;
}
export class SqlControl extends React.Component<SqlControlProps, SqlControlState> {
@ -54,8 +76,10 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
super(props, context);
this.state = {
query: props.initSql || '',
autoCompleteOn: true,
autoCompleteLoading: false
autoComplete: true,
autoCompleteLoading: false,
bypassCache: false,
wrapQuery: true
};
}
@ -166,29 +190,49 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
});
}
render() {
const { onRun, onExplain, queryElapsed } = this.props;
const { query, autoCompleteOn } = this.state;
private onRunClick = () => {
const { onRun } = this.props;
const { query, bypassCache, wrapQuery } = this.state;
onRun(query, bypassCache, wrapQuery);
}
const isRune = query.trim().startsWith('{');
renderExtraMenu(isRune: boolean) {
const { onExplain } = this.props;
const { query, autoComplete, bypassCache, wrapQuery } = this.state;
const SqlControlPopover = <Popover position={Position.BOTTOM_LEFT}>
<Button minimal icon={IconNames.MORE}/>
<div className="sql-control-popover">
<Checkbox
checked={isRune ? false : autoCompleteOn}
label="Auto complete"
onChange={() => this.setState({autoCompleteOn: !autoCompleteOn})}
/>
<Button
return <Menu>
{
!isRune &&
<>
<MenuItem
icon={IconNames.CLEAN}
className={Classes.POPOVER_DISMISS}
text="Explain"
onClick={() => onExplain(query)}
minimal
/>
</div>
</Popover>;
<MenuCheckbox
checked={autoComplete}
label="Auto complete"
onChange={() => this.setState({autoComplete: !autoComplete})}
/>
<MenuCheckbox
checked={wrapQuery}
label="Wrap query with limit"
onChange={() => this.setState({wrapQuery: !wrapQuery})}
/>
</>
}
<MenuCheckbox
checked={bypassCache}
label="Bypass cache"
onChange={() => this.setState({bypassCache: !bypassCache})}
/>
</Menu>;
}
render() {
const { queryElapsed } = this.props;
const { query, autoComplete, wrapQuery } = this.state;
const isRune = query.trim().startsWith('{');
// Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
return <div className="sql-control">
@ -208,17 +252,24 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
$blockScrolling: Infinity
}}
setOptions={{
enableBasicAutocompletion: isRune ? false : autoCompleteOn,
enableLiveAutocompletion: isRune ? false : autoCompleteOn,
enableBasicAutocompletion: isRune ? false : autoComplete,
enableLiveAutocompletion: isRune ? false : autoComplete,
showLineNumbers: true,
tabSize: 2
}}
/>
<div className="buttons">
<Button rightIcon={IconNames.CARET_RIGHT} onClick={() => onRun(query)}>
{isRune ? 'Rune' : 'Run'}
</Button>
{!isRune && SqlControlPopover}
<ButtonGroup>
<Button
icon={IconNames.CARET_RIGHT}
onClick={this.onRunClick}
text={isRune ? 'Rune' : (wrapQuery ? 'Run with limit' : 'Run as is')}
disabled={isRune && !validHjson(query)}
/>
<Popover position={Position.BOTTOM_LEFT} content={this.renderExtraMenu(isRune)}>
<Button icon={IconNames.MORE}/>
</Popover>
</ButtonGroup>
{
queryElapsed &&
<span className={'query-elapsed'}> Last query took {(queryElapsed / 1000).toFixed(2)} seconds</span>

View File

@ -20,6 +20,8 @@ import { Button, Checkbox, FormGroup, Menu, Popover, Position } from '@blueprint
import { IconNames } from '@blueprintjs/icons';
import * as React from 'react';
import { MenuCheckbox } from './menu-checkbox';
import './table-column-selection.scss';
interface TableColumnSelectionProps extends React.Props<any> {
@ -44,16 +46,14 @@ export class TableColumnSelection extends React.Component<TableColumnSelectionPr
render() {
const { columns, onChange, tableColumnsHidden } = this.props;
const checkboxes = <Menu className="table-column-selection-menu">
<FormGroup>
{columns.map(column => (
<Checkbox
<MenuCheckbox
label={column}
key={column}
checked={!tableColumnsHidden.includes(column)}
onChange={() => onChange(column)}
/>
))}
</FormGroup>
</Menu>;
return <Popover

View File

@ -35,6 +35,12 @@ import {
import './sql-view.scss';
interface QueryWithFlags {
queryString: string;
bypassCache?: boolean;
wrapQuery?: boolean;
}
export interface SqlViewProps extends React.Props<any> {
initSql: string | null;
}
@ -57,7 +63,7 @@ interface SqlQueryResult {
export class SqlView extends React.Component<SqlViewProps, SqlViewState> {
private sqlQueryManager: QueryManager<string, SqlQueryResult>;
private sqlQueryManager: QueryManager<QueryWithFlags, SqlQueryResult>;
private explainQueryManager: QueryManager<string, any>;
constructor(props: SqlViewProps, context: any) {
@ -76,22 +82,46 @@ export class SqlView extends React.Component<SqlViewProps, SqlViewState> {
componentDidMount(): void {
this.sqlQueryManager = new QueryManager({
processQuery: async (query: string) => {
processQuery: async (queryWithFlags: QueryWithFlags) => {
const { queryString, bypassCache, wrapQuery } = queryWithFlags;
const startTime = new Date();
if (query.trim().startsWith('{')) {
if (queryString.trim().startsWith('{')) {
// Secret way to issue a native JSON "rune" query
const runeQuery = Hjson.parse(query);
const runeQuery = Hjson.parse(queryString);
if (bypassCache) {
runeQuery.context = runeQuery.context || {};
runeQuery.context.useCache = false;
runeQuery.context.populateCache = false;
}
const result = await queryDruidRune(runeQuery);
return {
queryResult: decodeRune(runeQuery, result),
queryElapsed: new Date().valueOf() - startTime.valueOf()
};
} else {
const result = await queryDruidSql({
query,
const actualQuery = wrapQuery ?
`SELECT * FROM (${queryString.trim().replace(/;+$/, '')}) LIMIT 5000` :
queryString;
const queryPayload: Record<string, any> = {
query: actualQuery,
resultFormat: 'array',
header: true
});
};
if (wrapQuery) {
queryPayload.context = {
useCache: false,
populateCache: false
};
}
const result = await queryDruidSql(queryPayload);
return {
queryResult: {
header: (result && result.length) ? result[0] : [],
@ -178,11 +208,11 @@ export class SqlView extends React.Component<SqlViewProps, SqlViewState> {
return <div className="sql-view app-view">
<SqlControl
initSql={initSql || localStorageGet(LocalStorageKeys.QUERY_KEY)}
onRun={q => {
localStorageSet(LocalStorageKeys.QUERY_KEY, q);
this.sqlQueryManager.runQuery(q);
onRun={(queryString, bypassCache, wrapQuery) => {
localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
this.sqlQueryManager.runQuery({ queryString, bypassCache, wrapQuery });
}}
onExplain={(q: string) => this.getExplain(q)}
onExplain={this.getExplain}
queryElapsed={queryElapsed}
/>
{this.renderResultTable()}