You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by cw...@apache.org on 2019/06/15 20:51:31 UTC
[incubator-druid] branch master updated: Web console: Add download
path to SQL Query (#7898)
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 <je...@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-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org