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 {
|
||||
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], {
|
||||
type: `text/${type}`
|
||||
type: blobType
|
||||
});
|
||||
FileSaver.saveAs(blob, fileName);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ exports[`sql view matches snapshot 1`] = `
|
|||
>
|
||||
<HotkeysTarget(SqlControl)
|
||||
initSql="test"
|
||||
onDownload={[Function]}
|
||||
onExplain={[Function]}
|
||||
onRun={[Function]}
|
||||
queryElapsed={null}
|
||||
|
|
|
@ -169,6 +169,38 @@ exports[`sql control matches snapshot 1`] = `
|
|||
>
|
||||
Last query took 0.00 seconds
|
||||
</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>
|
||||
`;
|
||||
|
|
|
@ -40,16 +40,17 @@
|
|||
bottom: 0;
|
||||
height: 30px;
|
||||
|
||||
button {
|
||||
margin-right: 15px;
|
||||
.query-elapsed {
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
}
|
||||
|
||||
.query-elapsed {
|
||||
.download-button {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ace_tooltip {
|
||||
|
@ -69,3 +70,7 @@
|
|||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-menu.download-format-menu {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ describe('sql control', () => {
|
|||
onRun={(query, context, wrapQuery) => {}}
|
||||
onExplain={(sqlQuery, context) => {}}
|
||||
queryElapsed={2}
|
||||
onDownload={() => {}}
|
||||
/>;
|
||||
|
||||
const { container } = render(sqlControl);
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
ButtonGroup,
|
||||
Intent, IResizeEntry,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuItem, NavbarGroup,
|
||||
Popover,
|
||||
Position, ResizeSensor
|
||||
} from '@blueprintjs/core';
|
||||
|
@ -57,6 +57,7 @@ export interface SqlControlProps extends React.Props<any> {
|
|||
onRun: (query: string, context: Record<string, any>, wrapQuery: boolean) => void;
|
||||
onExplain: (sqlQuery: string, context: Record<string, any>) => void;
|
||||
queryElapsed: number | null;
|
||||
onDownload: (format: string) => void;
|
||||
}
|
||||
|
||||
export interface SqlControlState {
|
||||
|
@ -313,9 +314,14 @@ export class SqlControl extends React.PureComponent<SqlControlProps, SqlControlS
|
|||
}
|
||||
|
||||
render() {
|
||||
const { queryElapsed } = this.props;
|
||||
const { queryElapsed, onDownload } = this.props;
|
||||
const { query, autoComplete, wrapQuery, editorHeight } = this.state;
|
||||
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
|
||||
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`}
|
||||
</span>
|
||||
}
|
||||
{
|
||||
queryElapsed &&
|
||||
<Popover className="download-button" content={downloadMenu} position={Position.BOTTOM_RIGHT}>
|
||||
<Button
|
||||
icon={IconNames.DOWNLOAD}
|
||||
minimal
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import { QueryPlanDialog } from '../../dialogs';
|
|||
import {
|
||||
BasicQueryExplanation,
|
||||
decodeRune,
|
||||
downloadFile,
|
||||
HeaderRows,
|
||||
localStorageGet, LocalStorageKeys,
|
||||
localStorageSet, parseQueryPlan,
|
||||
|
@ -169,6 +170,42 @@ export class SqlView extends React.PureComponent<SqlViewProps, SqlViewState> {
|
|||
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() {
|
||||
const {explainDialogOpen, explainResult, loadingExplain, explainError} = this.state;
|
||||
if (!loadingExplain && explainDialogOpen) {
|
||||
|
@ -226,6 +263,7 @@ export class SqlView extends React.PureComponent<SqlViewProps, SqlViewState> {
|
|||
this.explainQueryManager.runQuery({ queryString, context });
|
||||
}}
|
||||
queryElapsed={queryElapsed}
|
||||
onDownload={this.onDownload}
|
||||
/>
|
||||
</div>
|
||||
<div className="bottom-pane">
|
||||
|
|
Loading…
Reference in New Issue