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:
Jenny Zhu 2019-06-15 13:51:22 -07:00 committed by Clint Wylie
parent 8e5003b01c
commit f603498e11
7 changed files with 111 additions and 8 deletions

View File

@ -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);
}

View File

@ -18,6 +18,7 @@ exports[`sql view matches snapshot 1`] = `
>
<HotkeysTarget(SqlControl)
initSql="test"
onDownload={[Function]}
onExplain={[Function]}
onRun={[Function]}
queryElapsed={null}

View File

@ -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>
`;

View File

@ -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;
}

View File

@ -28,6 +28,7 @@ describe('sql control', () => {
onRun={(query, context, wrapQuery) => {}}
onExplain={(sqlQuery, context) => {}}
queryElapsed={2}
onDownload={() => {}}
/>;
const { container } = render(sqlControl);

View File

@ -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>;
}

View File

@ -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">