This is an automated email from the ASF dual-hosted git repository.

vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new ec28672d07 Web console: allow format picking for download (#14794)
ec28672d07 is described below

commit ec28672d072eeb75ac1a4d84a201c7d051ebe629
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Fri Aug 11 09:43:29 2023 -0700

    Web console: allow format picking for download (#14794)
    
    * allow format picking for download
    
    * better popover
    
    * ux review tweaks
---
 .../destination-pages-dialog.tsx                   |  7 +-
 .../destination-pages-pane.tsx                     | 83 ++++++++++++++++++----
 2 files changed, 70 insertions(+), 20 deletions(-)

diff --git 
a/web-console/src/views/workbench-view/destination-pages-dialog/destination-pages-dialog.tsx
 
b/web-console/src/views/workbench-view/destination-pages-dialog/destination-pages-dialog.tsx
index 1e40aefdd0..68c66487e3 100644
--- 
a/web-console/src/views/workbench-view/destination-pages-dialog/destination-pages-dialog.tsx
+++ 
b/web-console/src/views/workbench-view/destination-pages-dialog/destination-pages-dialog.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Button, Classes, Dialog } from '@blueprintjs/core';
+import { Classes, Dialog } from '@blueprintjs/core';
 import React from 'react';
 
 import type { Execution } from '../../../druid-models';
@@ -39,11 +39,6 @@ export const DestinationPagesDialog = React.memo(function 
DestinationPagesDialog
       <div className={Classes.DIALOG_BODY}>
         <DestinationPagesPane execution={execution} />
       </div>
-      <div className={Classes.DIALOG_FOOTER}>
-        <div className={Classes.DIALOG_FOOTER_ACTIONS}>
-          <Button text="Close" onClick={onClose} />
-        </div>
-      </div>
     </Dialog>
   );
 });
diff --git 
a/web-console/src/views/workbench-view/destination-pages-pane/destination-pages-pane.tsx
 
b/web-console/src/views/workbench-view/destination-pages-pane/destination-pages-pane.tsx
index 20f68a8af6..f850097da2 100644
--- 
a/web-console/src/views/workbench-view/destination-pages-pane/destination-pages-pane.tsx
+++ 
b/web-console/src/views/workbench-view/destination-pages-pane/destination-pages-pane.tsx
@@ -16,12 +16,14 @@
  * limitations under the License.
  */
 
-import { AnchorButton, Button } from '@blueprintjs/core';
+import { AnchorButton, Button, Intent, Menu, MenuItem, Position } from 
'@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import React from 'react';
+import { Popover2 } from '@blueprintjs/popover2';
+import React, { useState } from 'react';
 import ReactTable from 'react-table';
 
 import type { Execution } from '../../../druid-models';
+import { SMALL_TABLE_PAGE_SIZE } from '../../../react-table';
 import { Api, UrlBaser } from '../../../singletons';
 import {
   clamp,
@@ -29,10 +31,36 @@ import {
   formatBytes,
   formatInteger,
   pluralIfNeeded,
+  tickIcon,
   wait,
 } from '../../../utils';
 
-const MAX_DETAIL_ROWS = 20;
+type ResultFormat = 'object' | 'array' | 'objectLines' | 'arrayLines' | 'csv';
+
+const RESULT_FORMATS: ResultFormat[] = ['objectLines', 'object', 'arrayLines', 
'array', 'csv'];
+
+function resultFormatToExtension(resultFormat: ResultFormat): string {
+  switch (resultFormat) {
+    case 'object':
+    case 'array':
+      return 'json';
+
+    case 'objectLines':
+    case 'arrayLines':
+      return 'jsonl';
+
+    case 'csv':
+      return 'csv';
+  }
+}
+
+const RESULT_FORMAT_LABEL: Record<ResultFormat, string> = {
+  object: 'Array of objects',
+  array: 'Array of arrays',
+  objectLines: 'JSON Lines',
+  arrayLines: 'JSON Lines but every row is an array',
+  csv: 'CSV',
+};
 
 interface DestinationPagesPaneProps {
   execution: Execution;
@@ -42,6 +70,9 @@ export const DestinationPagesPane = React.memo(function 
DestinationPagesPane(
   props: DestinationPagesPaneProps,
 ) {
   const { execution } = props;
+  const [desiredResultFormat, setDesiredResultFormat] = 
useState<ResultFormat>('objectLines');
+  const desiredExtension = resultFormatToExtension(desiredResultFormat);
+
   const destination = execution.destination;
   const pages = execution.destinationPages;
   if (!pages) return null;
@@ -50,11 +81,13 @@ export const DestinationPagesPane = React.memo(function 
DestinationPagesPane(
   const numTotalRows = destination?.numTotalRows;
 
   function getPageUrl(pageIndex: number) {
-    return 
UrlBaser.base(`/druid/v2/sql/statements/${id}/results?page=${pageIndex}`);
+    return UrlBaser.base(
+      
`/druid/v2/sql/statements/${id}/results?page=${pageIndex}&resultFormat=${desiredResultFormat}`,
+    );
   }
 
   function getPageFilename(pageIndex: number) {
-    return `${id}_page${pageIndex}.jsonl`;
+    return `${id}_page${pageIndex}.${desiredExtension}`;
   }
 
   async function downloadAllPages() {
@@ -71,23 +104,46 @@ export const DestinationPagesPane = React.memo(function 
DestinationPagesPane(
         {`${
           typeof numTotalRows === 'number' ? pluralIfNeeded(numTotalRows, 
'row') : 'Results'
         } have been written to ${pluralIfNeeded(pages.length, 'page')}. `}
+      </p>
+      <p>
+        Format when downloading:{' '}
+        <Popover2
+          minimal
+          position={Position.BOTTOM_LEFT}
+          content={
+            <Menu>
+              {RESULT_FORMATS.map((resultFormat, i) => (
+                <MenuItem
+                  key={i}
+                  icon={tickIcon(desiredResultFormat === resultFormat)}
+                  text={RESULT_FORMAT_LABEL[resultFormat]}
+                  label={resultFormat}
+                  onClick={() => setDesiredResultFormat(resultFormat)}
+                />
+              ))}
+            </Menu>
+          }
+        >
+          <Button
+            text={RESULT_FORMAT_LABEL[desiredResultFormat]}
+            rightIcon={IconNames.CARET_DOWN}
+          />
+        </Popover2>{' '}
         {pages.length > 1 && (
           <Button
+            intent={Intent.PRIMARY}
             icon={IconNames.DOWNLOAD}
-            text={`Download all data (as ${pluralIfNeeded(pages.length, 
'file')})`}
-            minimal
+            text={`Download all data (${pluralIfNeeded(pages.length, 
'file')})`}
             onClick={() => void downloadAllPages()}
           />
         )}
       </p>
       <ReactTable
-        className="padded-header"
         data={pages}
         loading={false}
-        sortable
-        defaultSorted={[{ id: 'id', desc: false }]}
-        defaultPageSize={clamp(pages.length, 1, MAX_DETAIL_ROWS)}
-        showPagination={pages.length > MAX_DETAIL_ROWS}
+        sortable={false}
+        defaultPageSize={clamp(pages.length, 1, SMALL_TABLE_PAGE_SIZE)}
+        showPagination={pages.length > SMALL_TABLE_PAGE_SIZE}
         columns={[
           {
             Header: 'Page number',
@@ -116,12 +172,11 @@ export const DestinationPagesPane = React.memo(function 
DestinationPagesPane(
             Header: '',
             id: 'download',
             accessor: 'id',
-            sortable: false,
             width: 300,
             Cell: ({ value }) => (
               <AnchorButton
                 icon={IconNames.DOWNLOAD}
-                text="download .jsonl"
+                text="Download"
                 minimal
                 href={getPageUrl(value)}
                 download={getPageFilename(value)}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to