This is an automated email from the ASF dual-hosted git repository. cwylie pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-druid.git
The following commit(s) were added to refs/heads/master by this push: new f603498 Web console: Add download path to SQL Query (#7898) f603498 is described below commit f603498e113e511502c0aab6ca33a20337a29d74 Author: Jenny Zhu <jenny....@imply.io> AuthorDate: Sat Jun 15 13:51:22 2019 -0700 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 --- web-console/src/utils/general.tsx | 13 +++++++- .../sql-view/__snapshots__/sql-view.spec.tsx.snap | 1 + .../__snapshots__/sql-control.spec.tsx.snap | 32 ++++++++++++++++++ .../views/sql-view/sql-control/sql-control.scss | 15 ++++++--- .../sql-view/sql-control/sql-control.spec.tsx | 1 + .../src/views/sql-view/sql-control/sql-control.tsx | 19 +++++++++-- web-console/src/views/sql-view/sql-view.tsx | 38 ++++++++++++++++++++++ 7 files changed, 111 insertions(+), 8 deletions(-) diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 9037832..94c3427 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -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); } diff --git a/web-console/src/views/sql-view/__snapshots__/sql-view.spec.tsx.snap b/web-console/src/views/sql-view/__snapshots__/sql-view.spec.tsx.snap old mode 100755 new mode 100644 index a2592b3..737e993 --- a/web-console/src/views/sql-view/__snapshots__/sql-view.spec.tsx.snap +++ b/web-console/src/views/sql-view/__snapshots__/sql-view.spec.tsx.snap @@ -18,6 +18,7 @@ exports[`sql view matches snapshot 1`] = ` > <HotkeysTarget(SqlControl) initSql="test" + onDownload={[Function]} onExplain={[Function]} onRun={[Function]} queryElapsed={null} diff --git a/web-console/src/views/sql-view/sql-control/__snapshots__/sql-control.spec.tsx.snap b/web-console/src/views/sql-view/sql-control/__snapshots__/sql-control.spec.tsx.snap index 80ab5c4..98e7d7c 100644 --- a/web-console/src/views/sql-view/sql-control/__snapshots__/sql-control.spec.tsx.snap +++ b/web-console/src/views/sql-view/sql-control/__snapshots__/sql-control.spec.tsx.snap @@ -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> `; diff --git a/web-console/src/views/sql-view/sql-control/sql-control.scss b/web-console/src/views/sql-view/sql-control/sql-control.scss index 95c798b..7b54424 100644 --- a/web-console/src/views/sql-view/sql-control/sql-control.scss +++ b/web-console/src/views/sql-view/sql-control/sql-control.scss @@ -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; +} diff --git a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx b/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx index 1ac07be..a044aa4 100644 --- a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx +++ b/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx @@ -28,6 +28,7 @@ describe('sql control', () => { onRun={(query, context, wrapQuery) => {}} onExplain={(sqlQuery, context) => {}} queryElapsed={2} + onDownload={() => {}} />; const { container } = render(sqlControl); diff --git a/web-console/src/views/sql-view/sql-control/sql-control.tsx b/web-console/src/views/sql-view/sql-control/sql-control.tsx index 44730b9..58486a3 100644 --- a/web-console/src/views/sql-view/sql-control/sql-control.tsx +++ b/web-console/src/views/sql-view/sql-control/sql-control.tsx @@ -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>; } diff --git a/web-console/src/views/sql-view/sql-view.tsx b/web-console/src/views/sql-view/sql-view.tsx index 43f3262..d132080 100644 --- a/web-console/src/views/sql-view/sql-view.tsx +++ b/web-console/src/views/sql-view/sql-view.tsx @@ -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"> --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@druid.apache.org For additional commands, e-mail: commits-h...@druid.apache.org