mirror of https://github.com/apache/druid.git
Web console: Add download path to SQL Query (#7898)
* adding download path to Query * add more checker * updated snap tests * change after Vad's comments
This commit is contained in:
parent
8e5003b01c
commit
f603498e11
|
@ -248,8 +248,19 @@ export function sortWithPrefixSuffix(things: string[], prefix: string[], suffix:
|
||||||
// ----------------------------
|
// ----------------------------
|
||||||
|
|
||||||
export function downloadFile(text: string, type: string, fileName: string): void {
|
export function downloadFile(text: string, type: string, fileName: string): void {
|
||||||
|
let blobType: string = '';
|
||||||
|
switch (type) {
|
||||||
|
case 'json':
|
||||||
|
blobType = 'application/json';
|
||||||
|
break;
|
||||||
|
case 'tsv':
|
||||||
|
blobType = 'text/tab-separated-values';
|
||||||
|
break;
|
||||||
|
default: // csv
|
||||||
|
blobType = `text/${type}`;
|
||||||
|
}
|
||||||
const blob = new Blob([text], {
|
const blob = new Blob([text], {
|
||||||
type: `text/${type}`
|
type: blobType
|
||||||
});
|
});
|
||||||
FileSaver.saveAs(blob, fileName);
|
FileSaver.saveAs(blob, fileName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ exports[`sql view matches snapshot 1`] = `
|
||||||
>
|
>
|
||||||
<HotkeysTarget(SqlControl)
|
<HotkeysTarget(SqlControl)
|
||||||
initSql="test"
|
initSql="test"
|
||||||
|
onDownload={[Function]}
|
||||||
onExplain={[Function]}
|
onExplain={[Function]}
|
||||||
onRun={[Function]}
|
onRun={[Function]}
|
||||||
queryElapsed={null}
|
queryElapsed={null}
|
||||||
|
|
|
@ -169,6 +169,38 @@ exports[`sql control matches snapshot 1`] = `
|
||||||
>
|
>
|
||||||
Last query took 0.00 seconds
|
Last query took 0.00 seconds
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class="bp3-popover-wrapper download-button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="bp3-popover-target"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="bp3-button bp3-minimal"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="bp3-icon bp3-icon-download"
|
||||||
|
icon="download"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
data-icon="download"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="16"
|
||||||
|
>
|
||||||
|
<desc>
|
||||||
|
download
|
||||||
|
</desc>
|
||||||
|
<path
|
||||||
|
d="M7.99-.01c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM11.7 9.7l-3 3c-.18.18-.43.29-.71.29s-.53-.11-.71-.29l-3-3A1.003 1.003 0 0 1 5.7 8.28l1.29 1.29V3.99c0-.55.45-1 1-1s1 .45 1 1v5.59l1.29-1.29a1.003 1.003 0 0 1 1.71.71c0 .27-.11.52-.29.7z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -40,16 +40,17 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|
||||||
button {
|
.query-elapsed {
|
||||||
margin-right: 15px;
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
|
right: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-elapsed {
|
.download-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace_tooltip {
|
.ace_tooltip {
|
||||||
|
@ -69,3 +70,7 @@
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bp3-menu.download-format-menu {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ describe('sql control', () => {
|
||||||
onRun={(query, context, wrapQuery) => {}}
|
onRun={(query, context, wrapQuery) => {}}
|
||||||
onExplain={(sqlQuery, context) => {}}
|
onExplain={(sqlQuery, context) => {}}
|
||||||
queryElapsed={2}
|
queryElapsed={2}
|
||||||
|
onDownload={() => {}}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const { container } = render(sqlControl);
|
const { container } = render(sqlControl);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Intent, IResizeEntry,
|
Intent, IResizeEntry,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem, NavbarGroup,
|
||||||
Popover,
|
Popover,
|
||||||
Position, ResizeSensor
|
Position, ResizeSensor
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
|
@ -57,6 +57,7 @@ export interface SqlControlProps extends React.Props<any> {
|
||||||
onRun: (query: string, context: Record<string, any>, wrapQuery: boolean) => void;
|
onRun: (query: string, context: Record<string, any>, wrapQuery: boolean) => void;
|
||||||
onExplain: (sqlQuery: string, context: Record<string, any>) => void;
|
onExplain: (sqlQuery: string, context: Record<string, any>) => void;
|
||||||
queryElapsed: number | null;
|
queryElapsed: number | null;
|
||||||
|
onDownload: (format: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SqlControlState {
|
export interface SqlControlState {
|
||||||
|
@ -313,9 +314,14 @@ export class SqlControl extends React.PureComponent<SqlControlProps, SqlControlS
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { queryElapsed } = this.props;
|
const { queryElapsed, onDownload } = this.props;
|
||||||
const { query, autoComplete, wrapQuery, editorHeight } = this.state;
|
const { query, autoComplete, wrapQuery, editorHeight } = this.state;
|
||||||
const isRune = query.trim().startsWith('{');
|
const isRune = query.trim().startsWith('{');
|
||||||
|
const downloadMenu = <Menu className="download-format-menu">
|
||||||
|
<MenuItem text="csv" onClick={() => onDownload('csv')} />
|
||||||
|
<MenuItem text="tsv" onClick={() => onDownload('tsv')} />
|
||||||
|
<MenuItem text="JSON" onClick={() => onDownload('json')}/>
|
||||||
|
</Menu>;
|
||||||
|
|
||||||
// Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
|
// Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
|
||||||
return <div className="sql-control">
|
return <div className="sql-control">
|
||||||
|
@ -364,6 +370,15 @@ export class SqlControl extends React.PureComponent<SqlControlProps, SqlControlS
|
||||||
{`Last query took ${(queryElapsed / 1000).toFixed(2)} seconds`}
|
{`Last query took ${(queryElapsed / 1000).toFixed(2)} seconds`}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
queryElapsed &&
|
||||||
|
<Popover className="download-button" content={downloadMenu} position={Position.BOTTOM_RIGHT}>
|
||||||
|
<Button
|
||||||
|
icon={IconNames.DOWNLOAD}
|
||||||
|
minimal
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { QueryPlanDialog } from '../../dialogs';
|
||||||
import {
|
import {
|
||||||
BasicQueryExplanation,
|
BasicQueryExplanation,
|
||||||
decodeRune,
|
decodeRune,
|
||||||
|
downloadFile,
|
||||||
HeaderRows,
|
HeaderRows,
|
||||||
localStorageGet, LocalStorageKeys,
|
localStorageGet, LocalStorageKeys,
|
||||||
localStorageSet, parseQueryPlan,
|
localStorageSet, parseQueryPlan,
|
||||||
|
@ -169,6 +170,42 @@ export class SqlView extends React.PureComponent<SqlViewProps, SqlViewState> {
|
||||||
localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
|
localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatStr(s: string | number, format: 'csv' | 'tsv') {
|
||||||
|
if (format === 'csv') {
|
||||||
|
// remove line break, single quote => double quote, handle ','
|
||||||
|
return `"${s.toString().replace(/(?:\r\n|\r|\n)/g, ' ').replace(/"/g, '""')}"`;
|
||||||
|
} else { // tsv
|
||||||
|
// remove line break, single quote => double quote, \t => ''
|
||||||
|
return `${s.toString().replace(/(?:\r\n|\r|\n)/g, ' ').replace(/\t/g, '').replace(/"/g, '""')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownload = (format: string) => {
|
||||||
|
const { result } = this.state;
|
||||||
|
if (!result) return;
|
||||||
|
let data: string = '';
|
||||||
|
let seperator: string = '';
|
||||||
|
const lineBreak = '\n';
|
||||||
|
|
||||||
|
if (format === 'csv' || format === 'tsv') {
|
||||||
|
seperator = format === 'csv' ? ',' : '\t';
|
||||||
|
data = result.header.map(str => this.formatStr(str, format)).join(seperator) + lineBreak;
|
||||||
|
data += result.rows.map(r => r.map(cell => this.formatStr(cell, format)).join(seperator)).join(lineBreak);
|
||||||
|
} else { // json
|
||||||
|
data = result.rows.map(r => {
|
||||||
|
const outputObject: Record<string, any> = {};
|
||||||
|
for (let k = 0; k < r.length; k++) {
|
||||||
|
const newName = result.header[k];
|
||||||
|
if (newName) {
|
||||||
|
outputObject[newName] = r[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.stringify(outputObject);
|
||||||
|
}).join(lineBreak);
|
||||||
|
}
|
||||||
|
downloadFile(data, format, 'query_result.' + format);
|
||||||
|
}
|
||||||
|
|
||||||
renderExplainDialog() {
|
renderExplainDialog() {
|
||||||
const {explainDialogOpen, explainResult, loadingExplain, explainError} = this.state;
|
const {explainDialogOpen, explainResult, loadingExplain, explainError} = this.state;
|
||||||
if (!loadingExplain && explainDialogOpen) {
|
if (!loadingExplain && explainDialogOpen) {
|
||||||
|
@ -226,6 +263,7 @@ export class SqlView extends React.PureComponent<SqlViewProps, SqlViewState> {
|
||||||
this.explainQueryManager.runQuery({ queryString, context });
|
this.explainQueryManager.runQuery({ queryString, context });
|
||||||
}}
|
}}
|
||||||
queryElapsed={queryElapsed}
|
queryElapsed={queryElapsed}
|
||||||
|
onDownload={this.onDownload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="bottom-pane">
|
<div className="bottom-pane">
|
||||||
|
|
Loading…
Reference in New Issue