mirror of https://github.com/apache/druid.git
Wrap query with limit within the web console (#7449)
* wrap with limit * make actual menu checkbox component
This commit is contained in:
parent
3aae4aaf8b
commit
60dd75d3d9
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -71,7 +54,7 @@
|
|||
hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
|
||||
.function-doc-name {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
label={column}
|
||||
key={column}
|
||||
checked={!tableColumnsHidden.includes(column)}
|
||||
onChange={() => onChange(column)}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
{columns.map(column => (
|
||||
<MenuCheckbox
|
||||
label={column}
|
||||
key={column}
|
||||
checked={!tableColumnsHidden.includes(column)}
|
||||
onChange={() => onChange(column)}
|
||||
/>
|
||||
))}
|
||||
</Menu>;
|
||||
|
||||
return <Popover
|
||||
|
|
|
@ -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()}
|
||||
|
|
Loading…
Reference in New Issue