Skip to content

Commit

Permalink
Web console: allow format picking for download (apache#14794)
Browse files Browse the repository at this point in the history
* allow format picking for download

* better popover

* ux review tweaks
  • Loading branch information
vogievetsky authored Aug 11, 2023
1 parent b0c78ff commit ec28672
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,51 @@
* 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,
downloadUrl,
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;
Expand All @@ -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;
Expand All @@ -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() {
Expand All @@ -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',
Expand Down Expand Up @@ -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)}
Expand Down

0 comments on commit ec28672

Please sign in to comment.