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

Reply via email to