Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`sql view matches snapshot 1`] = `
exports[`QueryView matches snapshot 1`] = `
<div
className="query-view app-view"
>
Expand Down Expand Up @@ -63,6 +63,7 @@ exports[`sql view matches snapshot 1`] = `
liveQueryMode="auto"
onLiveQueryModeChange={[Function]}
/>
<Memo(QueryTimer) />
</div>
</div>
<div
Expand All @@ -77,7 +78,7 @@ exports[`sql view matches snapshot 1`] = `
</div>
`;

exports[`sql view matches snapshot with query 1`] = `
exports[`QueryView matches snapshot with query 1`] = `
<div
className="query-view app-view"
>
Expand Down Expand Up @@ -140,6 +141,7 @@ exports[`sql view matches snapshot with query 1`] = `
liveQueryMode="auto"
onLiveQueryModeChange={[Function]}
/>
<Memo(QueryTimer) />
</div>
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`query extra info matches snapshot 1`] = `
exports[`QueryExtraInfo matches snapshot 1`] = `
<div
class="query-extra-info"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ import React from 'react';

import { QueryExtraInfo } from './query-extra-info';

describe('query extra info', () => {
describe('QueryExtraInfo', () => {
it('matches snapshot', () => {
const queryExtraInfo = (
<QueryExtraInfo
queryResult={QueryResult.BLANK.attachQueryId(
'e3ee781b-c0b6-4385-9d99-a8a1994bebac',
).changeQueryDuration(8000)}
onDownload={() => {}}
onLoadMore={() => {}}
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
import { IconNames } from '@blueprintjs/icons';
import copy from 'copy-to-clipboard';
import { QueryResult } from 'druid-query-toolkit';
import React from 'react';
import React, { MouseEvent } from 'react';

import { AppToaster } from '../../../singletons';
import { pluralIfNeeded } from '../../../utils';
Expand All @@ -39,20 +39,29 @@ import './query-extra-info.scss';
export interface QueryExtraInfoProps {
queryResult: QueryResult;
onDownload: (filename: string, format: string) => void;
onLoadMore: () => void;
}

export const QueryExtraInfo = React.memo(function QueryExtraInfo(props: QueryExtraInfoProps) {
const { queryResult, onDownload } = props;
const { queryResult, onDownload, onLoadMore } = props;
const wrapQueryLimit = queryResult.getSqlOuterLimit();
const hasMoreResults = queryResult.getNumResults() === wrapQueryLimit;

function handleQueryInfoClick() {
const id = queryResult.queryId || queryResult.sqlQueryId;
if (!id) return;
function handleQueryInfoClick(e: MouseEvent<HTMLDivElement>) {
if (e.altKey) {
if (hasMoreResults) {
onLoadMore();
}
Comment on lines +51 to +54
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this do? something with limit wrapping or.. loading stuff? ... if i followed correctly back to handleLoadMore?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are in the state where the auto limit wrapper is applied (sqlOuterLimit set in the context) then alt clicking here will 10x the sqlOuterLimit something that now happens when you reach the last loaded page.

So if you are in this state:

image

And you Alt+click the query reruns and you get:

image

Alt+click again and:

image

} else {
const id = queryResult.queryId || queryResult.sqlQueryId;
if (!id) return;

copy(id, { format: 'text/plain' });
AppToaster.show({
message: 'Query ID copied to clipboard',
intent: Intent.SUCCESS,
});
copy(id, { format: 'text/plain' });
AppToaster.show({
message: 'Query ID copied to clipboard',
intent: Intent.SUCCESS,
});
}
}

function handleDownload(format: string) {
Expand All @@ -71,13 +80,9 @@ export const QueryExtraInfo = React.memo(function QueryExtraInfo(props: QueryExt
</Menu>
);

const wrapQueryLimit = queryResult.getSqlOuterLimit();
let resultCount: string;
if (wrapQueryLimit && queryResult.getNumResults() === wrapQueryLimit) {
resultCount = `${queryResult.getNumResults() - 1}+ results`;
} else {
resultCount = pluralIfNeeded(queryResult.getNumResults(), 'result');
}
const resultCount = hasMoreResults
? `${queryResult.getNumResults() - 1}+ results`
: pluralIfNeeded(queryResult.getNumResults(), 'result');

let tooltipContent: JSX.Element | undefined;
if (queryResult.queryId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
@import '../../../blueprint-overrides/common/colors';

.query-output {
&.more-results .-totalPages {
// Hide the total page counter as it can be confusing due to the auto limit
display: none;
}

.ReactTable {
position: absolute;
top: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ ORDER BY "Count" DESC`);
false,
true,
).attachQuery({}, parsedQuery)}
onQueryChange={() => null}
onQueryChange={() => {}}
onLoadMore={() => {}}
/>
);

Expand Down
73 changes: 50 additions & 23 deletions web-console/src/views/query-view/query-output/query-output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import { Icon, Menu, MenuItem, Popover } from '@blueprintjs/core';
import { IconName, IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import {
QueryResult,
SqlExpression,
Expand All @@ -27,7 +28,7 @@ import {
trimString,
} from 'druid-query-toolkit';
import * as JSONBig from 'json-bigint-native';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import ReactTable from 'react-table';

import { BracedText, TableCell } from '../../../components';
Expand Down Expand Up @@ -60,42 +61,54 @@ interface Pagination {
pageSize: number;
}

function changePage(pagination: Pagination, page: number): Pagination {
return deepSet(pagination, 'page', page);
}

function getNumericColumnBraces(
queryResult: QueryResult | undefined,
queryResult: QueryResult,
pagination: Pagination,
): Record<number, string[]> {
const numericColumnBraces: Record<number, string[]> = {};
if (queryResult) {
const index = pagination.page * pagination.pageSize;
const rows = queryResult.rows.slice(index, index + pagination.pageSize);
if (rows.length) {
const numColumns = queryResult.header.length;
for (let c = 0; c < numColumns; c++) {
const brace = filterMap(rows, row =>
typeof row[c] === 'number' ? String(row[c]) : undefined,
);
if (rows.length === brace.length) {
numericColumnBraces[c] = brace;
}

const index = pagination.page * pagination.pageSize;
const rows = queryResult.rows.slice(index, index + pagination.pageSize);
if (rows.length) {
const numColumns = queryResult.header.length;
for (let c = 0; c < numColumns; c++) {
const brace = filterMap(rows, row =>
typeof row[c] === 'number' ? String(row[c]) : undefined,
);
if (rows.length === brace.length) {
numericColumnBraces[c] = brace;
}
}
}

return numericColumnBraces;
}

export interface QueryOutputProps {
queryResult?: QueryResult;
queryResult: QueryResult;
onQueryChange: (query: SqlQuery, run?: boolean) => void;
onLoadMore: () => void;
runeMode: boolean;
}

export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputProps) {
const { queryResult, onQueryChange, runeMode } = props;
const parsedQuery = queryResult ? queryResult.sqlQuery : undefined;
const { queryResult, onQueryChange, onLoadMore, runeMode } = props;
const parsedQuery = queryResult.sqlQuery;
const [pagination, setPagination] = useState<Pagination>({ page: 0, pageSize: 20 });
const [showValue, setShowValue] = useState<string>();
const [renamingColumn, setRenamingColumn] = useState<number>(-1);

// Reset page to 0 if number of results changes
useEffect(() => {
if (pagination.page) {
setPagination(changePage(pagination, 0));
}
}, [queryResult.rows.length]);

function hasFilterOnHeader(header: string, headerIndex: number): boolean {
if (!parsedQuery || !parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) return false;

Expand Down Expand Up @@ -371,18 +384,32 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
}
}

const outerLimit = queryResult.getSqlOuterLimit();
const hasMoreResults = queryResult.rows.length === outerLimit;

function changePagination(pagination: Pagination) {
if (
hasMoreResults &&
Math.floor(queryResult.rows.length / pagination.pageSize) === pagination.page // on the last page
) {
onLoadMore();
}
setPagination(pagination);
}

const numericColumnBraces = getNumericColumnBraces(queryResult, pagination);
return (
<div className="query-output">
<div className={classNames('query-output', { 'more-results': hasMoreResults })}>
<ReactTable
data={queryResult ? (queryResult.rows as any[][]) : []}
noDataText={queryResult && !queryResult.rows.length ? 'Query returned no data' : ''}
data={queryResult.rows as any[][]}
noDataText={queryResult.rows.length ? '' : 'Query returned no data'}
page={pagination.page}
pageSize={pagination.pageSize}
onPageChange={page => setPagination(deepSet(pagination, 'page', page))}
onPageSizeChange={(pageSize, page) => setPagination({ page, pageSize })}
onPageChange={page => changePagination(changePage(pagination, page))}
onPageSizeChange={(pageSize, page) => changePagination({ page, pageSize })}
sortable={false}
columns={(queryResult ? queryResult.header : []).map((column, i) => {
ofText={hasMoreResults ? '' : 'of'}
columns={queryResult.header.map((column, i) => {
const h = column.name;
return {
Header:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`QueryTimer matches snapshot 1`] = `
<div
class="query-timer"
>
1.85s
<button
class="bp3-button bp3-minimal"
type="button"
>
<span
class="bp3-icon bp3-icon-stopwatch"
icon="stopwatch"
>
<svg
data-icon="stopwatch"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
stopwatch
</desc>
<path
d="M9 2v1.083A6.002 6.002 0 018 15 6 6 0 017 3.083V2H6a1 1 0 110-2h4a1 1 0 010 2H9zM8 5a4 4 0 104 4H8V5z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</div>
`;
23 changes: 23 additions & 0 deletions web-console/src/views/query-view/query-timer/query-timer.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.query-timer {
line-height: 30px;
white-space: nowrap;
pointer-events: none;
}
42 changes: 42 additions & 0 deletions web-console/src/views/query-view/query-timer/query-timer.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { render } from '@testing-library/react';
import React from 'react';

import { QueryTimer } from './query-timer';

describe('QueryTimer', () => {
beforeEach(() => {
let nowCalls = 0;
jest.spyOn(Date, 'now').mockImplementation(() => {
return 1619201218452 + 2000 * nowCalls++;
});
});

afterEach(() => {
jest.restoreAllMocks();
});

it('matches snapshot', () => {
const queryTimer = <QueryTimer />;

const { container } = render(queryTimer);
expect(container.firstChild).toMatchSnapshot();
});
});
Loading