Web console: query view improvements and other fixes (#12031)

* don't copy commas

* use numeric type information

* add VALUES keyword

* propogate rollup config into spec

* fix

* cleanup

* understand range partitioning

* update snapshots

* better comp apis

* fix segment pages

* update snapshots
This commit is contained in:
Vadim Ogievetsky 2021-12-07 10:16:16 -08:00 committed by GitHub
parent 0b3f0bbbd8
commit 1d3c8c187b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 547 additions and 156 deletions

View File

@ -5105,7 +5105,7 @@ license_category: binary
module: web-console module: web-console
license_name: MIT License license_name: MIT License
copyright: Matt Zabriskie copyright: Matt Zabriskie
version: 0.21.1 version: 0.21.4
license_file_path: licenses/bin/axios.MIT license_file_path: licenses/bin/axios.MIT
--- ---
@ -5294,7 +5294,7 @@ license_category: binary
module: web-console module: web-console
license_name: Apache License version 2.0 license_name: Apache License version 2.0
copyright: Imply Data copyright: Imply Data
version: 0.11.10 version: 0.14.4
--- ---
@ -5313,7 +5313,7 @@ license_category: binary
module: web-console module: web-console
license_name: MIT License license_name: MIT License
copyright: Ruben Verborgh copyright: Ruben Verborgh
version: 1.13.3 version: 1.14.4
license_file_path: licenses/bin/follow-redirects.MIT license_file_path: licenses/bin/follow-redirects.MIT
--- ---

View File

@ -53,6 +53,7 @@ exports.SQL_KEYWORDS = [
'ROW', 'ROW',
'ROWS', 'ROWS',
'ONLY', 'ONLY',
'VALUES',
]; ];
exports.SQL_EXPRESSION_PARTS = [ exports.SQL_EXPRESSION_PARTS = [

View File

@ -7970,9 +7970,9 @@
} }
}, },
"druid-query-toolkit": { "druid-query-toolkit": {
"version": "0.11.10", "version": "0.14.4",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.11.10.tgz", "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.14.4.tgz",
"integrity": "sha512-jKqec2YMxCVvow8e9lmmrRKXxq/ugyeyKTVPaAUPbjoP4VHxk55BS2gXJ/S2ysCeVgvyJbjGbg2ZIkUzg4Whuw==", "integrity": "sha512-PmD5vwoHQxNxZ8E8vRdHvh5OjuvA+yHD5dhiKDzIzPtnFiwRHLJKyOLSQ6rmN1VAKbOdU4JCZIzPFUB8bEMBAQ==",
"requires": { "requires": {
"tslib": "^2.2.0" "tslib": "^2.2.0"
} }

View File

@ -79,7 +79,7 @@
"d3-axis": "^1.0.12", "d3-axis": "^1.0.12",
"d3-scale": "^3.2.0", "d3-scale": "^3.2.0",
"d3-selection": "^1.4.0", "d3-selection": "^1.4.0",
"druid-query-toolkit": "^0.11.10", "druid-query-toolkit": "^0.14.4",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"fontsource-open-sans": "^3.0.9", "fontsource-open-sans": "^3.0.9",
"has-own-prop": "^2.0.0", "has-own-prop": "^2.0.0",

View File

@ -38,5 +38,9 @@
.zero-pad { .zero-pad {
visibility: hidden; visibility: hidden;
} }
.unselectable {
user-select: none;
}
} }
} }

View File

@ -17,14 +17,17 @@
*/ */
import { max } from 'd3-array'; import { max } from 'd3-array';
import React from 'react'; import React, { Fragment } from 'react';
import './braced-text.scss'; import './braced-text.scss';
const THOUSANDS_SEPARATOR = ','; // Maybe one day make this locale aware
export interface BracedTextProps { export interface BracedTextProps {
text: string; text: string;
braces: string[]; braces: string[];
padFractionalPart?: boolean; padFractionalPart?: boolean;
unselectableThousandsSeparator?: boolean;
} }
export function findMostNumbers(strings: string[]): string { export function findMostNumbers(strings: string[]): string {
@ -56,8 +59,29 @@ function zerosOfLength(n: number): string {
return new Array(n + 1).join('0'); return new Array(n + 1).join('0');
} }
function arrayJoin<T, U>(array: T[], separator: U): (T | U)[] {
const result: (T | U)[] = [];
for (let i = 0; i < array.length; i++) {
if (i) {
result.push(separator, array[i]);
} else {
result.push(array[i]);
}
}
return result;
}
function hideThousandsSeparator(text: string) {
const parts = text.split(THOUSANDS_SEPARATOR);
if (parts.length < 2) return text;
return arrayJoin(
parts,
<span className="unselectable">{THOUSANDS_SEPARATOR}</span>,
).map((x, i) => <Fragment key={i}>{x}</Fragment>);
}
export const BracedText = React.memo(function BracedText(props: BracedTextProps) { export const BracedText = React.memo(function BracedText(props: BracedTextProps) {
const { text, braces, padFractionalPart } = props; const { text, braces, padFractionalPart, unselectableThousandsSeparator } = props;
let effectiveBraces = braces.concat(text); let effectiveBraces = braces.concat(text);
@ -90,7 +114,7 @@ export const BracedText = React.memo(function BracedText(props: BracedTextProps)
<span className="braced-text"> <span className="braced-text">
<span className="brace-text">{findMostNumbers(effectiveBraces)}</span> <span className="brace-text">{findMostNumbers(effectiveBraces)}</span>
<span className="real-text"> <span className="real-text">
{text} {unselectableThousandsSeparator ? hideThousandsSeparator(text) : text}
{zeroPad} {zeroPad}
</span> </span>
</span> </span>

View File

@ -19,7 +19,7 @@
import React from 'react'; import React from 'react';
export interface DeferredProps { export interface DeferredProps {
content: () => JSX.Element; content: () => JSX.Element | null;
} }
export const Deferred = React.memo(function Deferred(props: DeferredProps) { export const Deferred = React.memo(function Deferred(props: DeferredProps) {

View File

@ -23,6 +23,7 @@ export * from './auto-form/auto-form';
export * from './braced-text/braced-text'; export * from './braced-text/braced-text';
export * from './center-message/center-message'; export * from './center-message/center-message';
export * from './clearable-input/clearable-input'; export * from './clearable-input/clearable-input';
export * from './deferred/deferred';
export * from './external-link/external-link'; export * from './external-link/external-link';
export * from './form-json-selector/form-json-selector'; export * from './form-json-selector/form-json-selector';
export * from './formatted-input/formatted-input'; export * from './formatted-input/formatted-input';

View File

@ -16,11 +16,13 @@
* limitations under the License. * limitations under the License.
*/ */
import classNames from 'classnames';
import React from 'react'; import React from 'react';
import './loader.scss'; import './loader.scss';
export interface LoaderProps { export interface LoaderProps {
className?: string;
loading?: boolean; // This is needed so that this component can be used as a LoadingComponent in react table loading?: boolean; // This is needed so that this component can be used as a LoadingComponent in react table
loadingText?: string; loadingText?: string;
cancelText?: string; cancelText?: string;
@ -28,11 +30,11 @@ export interface LoaderProps {
} }
export const Loader = React.memo(function Loader(props: LoaderProps) { export const Loader = React.memo(function Loader(props: LoaderProps) {
const { loadingText, loading, cancelText, onCancel } = props; const { className, loadingText, loading, cancelText, onCancel } = props;
if (loading === false) return null; if (loading === false) return null;
return ( return (
<div className="loader"> <div className={classNames('loader', className)}>
<div className="loader-logo"> <div className="loader-logo">
<svg viewBox="0 0 100 100"> <svg viewBox="0 0 100 100">
<path <path

View File

@ -16,26 +16,26 @@
* limitations under the License. * limitations under the License.
*/ */
import { MenuItem } from '@blueprintjs/core'; import { MenuItem, MenuItemProps } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import React from 'react'; import React from 'react';
export interface MenuCheckboxProps { export interface MenuCheckboxProps extends Omit<MenuItemProps, 'icon' | 'onClick'> {
text: string;
checked: boolean; checked: boolean;
onChange: () => void; onChange: () => void;
} }
export function MenuCheckbox(props: MenuCheckboxProps) { export function MenuCheckbox(props: MenuCheckboxProps) {
const { text, checked, onChange } = props; const { checked, onChange, className, shouldDismissPopover, ...rest } = props;
return ( return (
<MenuItem <MenuItem
className="menu-checkbox" className={classNames('menu-checkbox', className)}
icon={checked ? IconNames.TICK_CIRCLE : IconNames.CIRCLE} icon={checked ? IconNames.TICK_CIRCLE : IconNames.CIRCLE}
text={text}
onClick={onChange} onClick={onChange}
shouldDismissPopover={false} shouldDismissPopover={shouldDismissPopover ?? false}
{...rest}
/> />
); );
} }

View File

@ -17,7 +17,7 @@
*/ */
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { sane } from 'druid-query-toolkit/build/test-utils'; import { sane } from 'druid-query-toolkit';
import React from 'react'; import React from 'react';
import { Capabilities } from '../../utils'; import { Capabilities } from '../../utils';

View File

@ -207,6 +207,15 @@ exports[`CompactionDialog matches snapshot with compactionConfig (dynamic partit
"required": true, "required": true,
"type": "string", "type": "string",
}, },
Object {
"defined": [Function],
"info": <p>
The dimensions to partition on.
</p>,
"name": "tuningConfig.partitionsSpec.partitionDimensions",
"required": true,
"type": "string-array",
},
Object { Object {
"defined": [Function], "defined": [Function],
"info": <React.Fragment> "info": <React.Fragment>
@ -568,6 +577,15 @@ exports[`CompactionDialog matches snapshot with compactionConfig (hashed partiti
"required": true, "required": true,
"type": "string", "type": "string",
}, },
Object {
"defined": [Function],
"info": <p>
The dimensions to partition on.
</p>,
"name": "tuningConfig.partitionsSpec.partitionDimensions",
"required": true,
"type": "string-array",
},
Object { Object {
"defined": [Function], "defined": [Function],
"info": <React.Fragment> "info": <React.Fragment>
@ -929,6 +947,15 @@ exports[`CompactionDialog matches snapshot with compactionConfig (single_dim par
"required": true, "required": true,
"type": "string", "type": "string",
}, },
Object {
"defined": [Function],
"info": <p>
The dimensions to partition on.
</p>,
"name": "tuningConfig.partitionsSpec.partitionDimensions",
"required": true,
"type": "string-array",
},
Object { Object {
"defined": [Function], "defined": [Function],
"info": <React.Fragment> "info": <React.Fragment>
@ -1290,6 +1317,15 @@ exports[`CompactionDialog matches snapshot without compactionConfig 1`] = `
"required": true, "required": true,
"type": "string", "type": "string",
}, },
Object {
"defined": [Function],
"info": <p>
The dimensions to partition on.
</p>,
"name": "tuningConfig.partitionsSpec.partitionDimensions",
"required": true,
"type": "string-array",
},
Object { Object {
"defined": [Function], "defined": [Function],
"info": <React.Fragment> "info": <React.Fragment>

View File

@ -16,7 +16,7 @@ exports[`clipboard dialog matches snapshot 1`] = `
tabindex="0" tabindex="0"
> >
<div <div
class="bp3-dialog show-value-dialog" class="bp3-dialog show-value-dialog normal"
> >
<div <div
class="bp3-dialog-header" class="bp3-dialog-header"
@ -54,6 +54,7 @@ exports[`clipboard dialog matches snapshot 1`] = `
</div> </div>
<textarea <textarea
class="bp3-input" class="bp3-input"
spellcheck="false"
> >
Bot: Automatska zamjena teksta (-[[Administrativna podjela Meksika|Admin]] +[[Administrativna podjela Meksika|Admi]]) Bot: Automatska zamjena teksta (-[[Administrativna podjela Meksika|Admin]] +[[Administrativna podjela Meksika|Admi]])
</textarea> </textarea>

View File

@ -21,9 +21,18 @@
padding-bottom: 10px; padding-bottom: 10px;
} }
&.normal.bp3-dialog {
height: 600px;
}
&.large.bp3-dialog {
width: 90vw;
height: 90vh;
}
.bp3-input { .bp3-input {
margin: 10px; margin: 10px;
height: 400px; flex: 1;
} }
.bp3-dialog-footer-actions { .bp3-dialog-footer-actions {

View File

@ -18,6 +18,7 @@
import { Button, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core'; import { Button, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import React from 'react'; import React from 'react';
@ -28,10 +29,11 @@ import './show-value-dialog.scss';
export interface ShowValueDialogProps { export interface ShowValueDialogProps {
onClose: () => void; onClose: () => void;
str: string; str: string;
size?: 'normal' | 'large';
} }
export const ShowValueDialog = React.memo(function ShowValueDialog(props: ShowValueDialogProps) { export const ShowValueDialog = React.memo(function ShowValueDialog(props: ShowValueDialogProps) {
const { onClose, str } = props; const { onClose, str, size } = props;
function handleCopy() { function handleCopy() {
copy(str, { format: 'text/plain' }); copy(str, { format: 'text/plain' });
@ -42,8 +44,13 @@ export const ShowValueDialog = React.memo(function ShowValueDialog(props: ShowVa
} }
return ( return (
<Dialog className="show-value-dialog" isOpen onClose={onClose} title="Full value"> <Dialog
<TextArea value={str} /> className={classNames('show-value-dialog', size || 'normal')}
isOpen
onClose={onClose}
title="Full value"
>
<TextArea value={str} spellCheck={false} />
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button icon={IconNames.DUPLICATE} text="Copy" onClick={handleCopy} /> <Button icon={IconNames.DUPLICATE} text="Copy" onClick={handleCopy} />
<Button text="Close" intent={Intent.PRIMARY} onClick={onClose} /> <Button text="Close" intent={Intent.PRIMARY} onClick={onClose} />

View File

@ -160,7 +160,7 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
defined: t => deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed', defined: t => deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed',
info: <p>The dimensions to partition on. Leave blank to select all dimensions.</p>, info: <p>The dimensions to partition on. Leave blank to select all dimensions.</p>,
}, },
// partitionsSpec type: single_dim // partitionsSpec type: single_dim, range
{ {
name: 'tuningConfig.partitionsSpec.partitionDimension', name: 'tuningConfig.partitionsSpec.partitionDimension',
type: 'string', type: 'string',
@ -168,12 +168,19 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
required: true, required: true,
info: <p>The dimension to partition on.</p>, info: <p>The dimension to partition on.</p>,
}, },
{
name: 'tuningConfig.partitionsSpec.partitionDimensions',
type: 'string-array',
defined: t => deepGet(t, 'tuningConfig.partitionsSpec.type') === 'range',
required: true,
info: <p>The dimensions to partition on.</p>,
},
{ {
name: 'tuningConfig.partitionsSpec.targetRowsPerSegment', name: 'tuningConfig.partitionsSpec.targetRowsPerSegment',
type: 'number', type: 'number',
zeroMeansUndefined: true, zeroMeansUndefined: true,
defined: t => defined: t =>
deepGet(t, 'tuningConfig.partitionsSpec.type') === 'single_dim' && oneOf(deepGet(t, 'tuningConfig.partitionsSpec.type'), 'single_dim', 'range') &&
!deepGet(t, 'tuningConfig.partitionsSpec.maxRowsPerSegment'), !deepGet(t, 'tuningConfig.partitionsSpec.maxRowsPerSegment'),
required: (t: CompactionConfig) => required: (t: CompactionConfig) =>
!deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment') && !deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment') &&
@ -196,7 +203,7 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
type: 'number', type: 'number',
zeroMeansUndefined: true, zeroMeansUndefined: true,
defined: t => defined: t =>
deepGet(t, 'tuningConfig.partitionsSpec.type') === 'single_dim' && oneOf(deepGet(t, 'tuningConfig.partitionsSpec.type'), 'single_dim', 'range') &&
!deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'), !deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'),
required: (t: CompactionConfig) => required: (t: CompactionConfig) =>
!deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment') && !deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment') &&
@ -215,7 +222,7 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
name: 'tuningConfig.partitionsSpec.assumeGrouped', name: 'tuningConfig.partitionsSpec.assumeGrouped',
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
defined: t => deepGet(t, 'tuningConfig.partitionsSpec.type') === 'single_dim', defined: t => oneOf(deepGet(t, 'tuningConfig.partitionsSpec.type'), 'single_dim', 'range'),
info: ( info: (
<p> <p>
Assume that input data has already been grouped on time and dimensions. Ingestion will run Assume that input data has already been grouped on time and dimensions. Ingestion will run

View File

@ -1230,6 +1230,40 @@ export function fillDataSourceNameIfNeeded(spec: Partial<IngestionSpec>): Partia
return deepSetIfUnset(spec, 'spec.dataSchema.dataSource', possibleName); return deepSetIfUnset(spec, 'spec.dataSchema.dataSource', possibleName);
} }
export function guessDataSourceNameFromInputSource(inputSource: InputSource): string | undefined {
switch (inputSource.type) {
case 'local':
if (inputSource.filter && filterIsFilename(inputSource.filter)) {
return basenameFromFilename(inputSource.filter);
} else if (inputSource.baseDir) {
return filenameFromPath(inputSource.baseDir);
} else {
return;
}
case 's3':
case 'azure':
case 'google': {
const actualPath = (inputSource.objects || EMPTY_ARRAY)[0];
const uriPath =
(inputSource.uris || EMPTY_ARRAY)[0] || (inputSource.prefixes || EMPTY_ARRAY)[0];
return actualPath ? actualPath.path : uriPath ? filenameFromPath(uriPath) : undefined;
}
case 'http':
return Array.isArray(inputSource.uris) ? filenameFromPath(inputSource.uris[0]) : undefined;
case 'druid':
return inputSource.dataSource;
case 'inline':
return 'inline_data';
default:
return;
}
}
export function guessDataSourceName(spec: Partial<IngestionSpec>): string | undefined { export function guessDataSourceName(spec: Partial<IngestionSpec>): string | undefined {
const ioConfig = deepGet(spec, 'spec.ioConfig'); const ioConfig = deepGet(spec, 'spec.ioConfig');
if (!ioConfig) return; if (!ioConfig) return;
@ -1239,39 +1273,7 @@ export function guessDataSourceName(spec: Partial<IngestionSpec>): string | unde
case 'index_parallel': { case 'index_parallel': {
const inputSource = ioConfig.inputSource; const inputSource = ioConfig.inputSource;
if (!inputSource) return; if (!inputSource) return;
return guessDataSourceNameFromInputSource(inputSource);
switch (inputSource.type) {
case 'local':
if (inputSource.filter && filterIsFilename(inputSource.filter)) {
return basenameFromFilename(inputSource.filter);
} else if (inputSource.baseDir) {
return filenameFromPath(inputSource.baseDir);
} else {
return;
}
case 's3':
case 'azure':
case 'google': {
const actualPath = (inputSource.objects || EMPTY_ARRAY)[0];
const uriPath =
(inputSource.uris || EMPTY_ARRAY)[0] || (inputSource.prefixes || EMPTY_ARRAY)[0];
return actualPath ? actualPath.path : uriPath ? filenameFromPath(uriPath) : undefined;
}
case 'http':
return Array.isArray(inputSource.uris)
? filenameFromPath(inputSource.uris[0])
: undefined;
case 'druid':
return inputSource.dataSource;
case 'inline':
return 'inline_data';
}
return;
} }
case 'kafka': case 'kafka':
@ -1340,7 +1342,7 @@ export function adjustForceGuaranteedRollup(spec: Partial<IngestionSpec>) {
const partitionsSpecType = deepGet(spec, 'spec.tuningConfig.partitionsSpec.type') || 'dynamic'; const partitionsSpecType = deepGet(spec, 'spec.tuningConfig.partitionsSpec.type') || 'dynamic';
if (partitionsSpecType === 'dynamic') { if (partitionsSpecType === 'dynamic') {
spec = deepDelete(spec, 'spec.tuningConfig.forceGuaranteedRollup'); spec = deepDelete(spec, 'spec.tuningConfig.forceGuaranteedRollup');
} else if (oneOf(partitionsSpecType, 'hashed', 'single_dim')) { } else if (oneOf(partitionsSpecType, 'hashed', 'single_dim', 'range')) {
spec = deepSet(spec, 'spec.tuningConfig.forceGuaranteedRollup', true); spec = deepSet(spec, 'spec.tuningConfig.forceGuaranteedRollup', true);
} }
@ -1523,12 +1525,19 @@ export function getSecondaryPartitionRelatedFormFields(
</> </>
), ),
}, },
{
name: 'spec.tuningConfig.partitionsSpec.partitionDimensions',
type: 'string-array',
defined: s => deepGet(s, 'spec.tuningConfig.partitionsSpec.type') === 'range',
required: true,
info: <p>The dimensions to partition on.</p>,
},
{ {
name: 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment', name: 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment',
type: 'number', type: 'number',
zeroMeansUndefined: true, zeroMeansUndefined: true,
defined: s => defined: s =>
deepGet(s, 'spec.tuningConfig.partitionsSpec.type') === 'single_dim' && oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'single_dim', 'range') &&
!deepGet(s, 'spec.tuningConfig.partitionsSpec.maxRowsPerSegment'), !deepGet(s, 'spec.tuningConfig.partitionsSpec.maxRowsPerSegment'),
required: s => required: s =>
!deepGet(s, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment') && !deepGet(s, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment') &&
@ -1545,7 +1554,7 @@ export function getSecondaryPartitionRelatedFormFields(
type: 'number', type: 'number',
zeroMeansUndefined: true, zeroMeansUndefined: true,
defined: s => defined: s =>
deepGet(s, 'spec.tuningConfig.partitionsSpec.type') === 'single_dim' && oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'single_dim', 'range') &&
!deepGet(s, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment'), !deepGet(s, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment'),
required: s => required: s =>
!deepGet(s, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment') && !deepGet(s, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment') &&
@ -1557,7 +1566,8 @@ export function getSecondaryPartitionRelatedFormFields(
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
hideInMore: true, hideInMore: true,
defined: s => deepGet(s, 'spec.tuningConfig.partitionsSpec.type') === 'single_dim', defined: s =>
oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'single_dim', 'range'),
info: ( info: (
<p> <p>
Assume that input data has already been grouped on time and dimensions. Ingestion will Assume that input data has already been grouped on time and dimensions. Ingestion will
@ -1627,10 +1637,8 @@ const TUNING_FORM_FIELDS: Field<IngestionSpec>[] = [
defaultValue: 10, defaultValue: 10,
min: 1, min: 1,
defined: s => defined: s =>
Boolean( s.type === 'index_parallel' &&
s.type === 'index_parallel' && oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'hashed', 'single_dim', 'range'),
oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'hashed', 'single_dim'),
),
info: <>Number of tasks to merge partial segments after shuffle.</>, info: <>Number of tasks to merge partial segments after shuffle.</>,
}, },
{ {
@ -1638,10 +1646,8 @@ const TUNING_FORM_FIELDS: Field<IngestionSpec>[] = [
type: 'number', type: 'number',
defaultValue: 100, defaultValue: 100,
defined: s => defined: s =>
Boolean( s.type === 'index_parallel' &&
s.type === 'index_parallel' && oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'hashed', 'single_dim', 'range'),
oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'hashed', 'single_dim'),
),
info: ( info: (
<> <>
Max limit for the number of segments a single task can merge at the same time after shuffle. Max limit for the number of segments a single task can merge at the same time after shuffle.

View File

@ -20,6 +20,7 @@ import 'core-js/stable';
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime';
import './bootstrap/ace'; import './bootstrap/ace';
import { QueryRunner } from 'druid-query-toolkit';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
@ -88,6 +89,10 @@ if (consoleConfig.linkOverrides) {
setLinkOverrides(consoleConfig.linkOverrides); setLinkOverrides(consoleConfig.linkOverrides);
} }
QueryRunner.defaultQueryExecutor = (payload, isSql, cancelToken) => {
return Api.instance.post(`/druid/v2${isSql ? '/sql' : ''}`, payload, { cancelToken });
};
ReactDOM.render( ReactDOM.render(
React.createElement(ConsoleApplication, { React.createElement(ConsoleApplication, {
exampleManifestsUrl: consoleConfig.exampleManifestsUrl, exampleManifestsUrl: consoleConfig.exampleManifestsUrl,

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { sane } from 'druid-query-toolkit/build/test-utils'; import { sane } from 'druid-query-toolkit';
import { DruidError, getDruidErrorMessage, parseHtmlError, trimSemicolon } from './druid-query'; import { DruidError, getDruidErrorMessage, parseHtmlError, trimSemicolon } from './druid-query';

View File

@ -167,6 +167,14 @@ export function typeIs<T extends { type?: S }, S = string>(...options: S[]): (x:
}; };
} }
export function without<T>(xs: readonly T[], x: T | undefined): T[] {
return xs.filter(i => i !== x);
}
export function change<T>(xs: readonly T[], from: T, to: T): T[] {
return xs.map(x => (x === from ? to : x));
}
// ---------------------------- // ----------------------------
export function countBy<T>( export function countBy<T>(
@ -246,6 +254,10 @@ export function formatInteger(n: NumberLike): string {
return numeral(n).format('0,0'); return numeral(n).format('0,0');
} }
export function formatNumber(n: NumberLike): string {
return n.toLocaleString('en-US', { maximumFractionDigits: 20 });
}
export function formatBytes(n: NumberLike): string { export function formatBytes(n: NumberLike): string {
return numeral(n).format('0.00 b'); return numeral(n).format('0.00 b');
} }
@ -262,6 +274,10 @@ export function formatPercent(n: NumberLike): string {
return (Number(n) * 100).toFixed(2) + '%'; return (Number(n) * 100).toFixed(2) + '%';
} }
export function formatPercentClapped(n: NumberLike): string {
return formatPercent(Math.min(Math.max(Number(n), 0), 1));
}
export function formatMillions(n: NumberLike): string { export function formatMillions(n: NumberLike): string {
const s = (Number(n) / 1e6).toFixed(3); const s = (Number(n) / 1e6).toFixed(3);
if (s === '0.000') return String(Math.round(Number(n))); if (s === '0.000') return String(Math.round(Number(n)));
@ -272,6 +288,10 @@ function pad2(str: string | number): string {
return ('00' + str).substr(-2); return ('00' + str).substr(-2);
} }
function pad3(str: string | number): string {
return ('000' + str).substr(-3);
}
export function formatDuration(ms: NumberLike): string { export function formatDuration(ms: NumberLike): string {
const n = Number(ms); const n = Number(ms);
const timeInHours = Math.floor(n / 3600000); const timeInHours = Math.floor(n / 3600000);
@ -280,6 +300,16 @@ export function formatDuration(ms: NumberLike): string {
return timeInHours + ':' + pad2(timeInMin) + ':' + pad2(timeInSec); return timeInHours + ':' + pad2(timeInMin) + ':' + pad2(timeInSec);
} }
export function formatDurationWithMs(ms: NumberLike): string {
const n = Number(ms);
const timeInHours = Math.floor(n / 3600000);
const timeInMin = Math.floor(n / 60000) % 60;
const timeInSec = Math.floor(n / 1000) % 60;
return (
timeInHours + ':' + pad2(timeInMin) + ':' + pad2(timeInSec) + '.' + pad3(Math.floor(n) % 1000)
);
}
export function pluralIfNeeded(n: NumberLike, singular: string, plural?: string): string { export function pluralIfNeeded(n: NumberLike, singular: string, plural?: string): string {
if (!plural) plural = singular + 's'; if (!plural) plural = singular + 's';
return `${formatInteger(n)} ${n === 1 ? singular : plural}`; return `${formatInteger(n)} ${n === 1 ? singular : plural}`;

View File

@ -30,3 +30,4 @@ export * from './query-cursor';
export * from './query-manager'; export * from './query-manager';
export * from './query-state'; export * from './query-state';
export * from './sanitizers'; export * from './sanitizers';
export * from './table-helpers';

View File

@ -286,6 +286,9 @@ export async function sampleForConnect(
dataSource: 'sample', dataSource: 'sample',
timestampSpec: reingestMode ? REINDEX_TIMESTAMP_SPEC : PLACEHOLDER_TIMESTAMP_SPEC, timestampSpec: reingestMode ? REINDEX_TIMESTAMP_SPEC : PLACEHOLDER_TIMESTAMP_SPEC,
dimensionsSpec: {}, dimensionsSpec: {},
granularitySpec: {
rollup: false,
},
}, },
} as any, } as any,
samplerConfig: BASE_SAMPLER_CONFIG, samplerConfig: BASE_SAMPLER_CONFIG,
@ -342,6 +345,9 @@ export async function sampleForParser(
dataSource: 'sample', dataSource: 'sample',
timestampSpec: reingestMode ? REINDEX_TIMESTAMP_SPEC : PLACEHOLDER_TIMESTAMP_SPEC, timestampSpec: reingestMode ? REINDEX_TIMESTAMP_SPEC : PLACEHOLDER_TIMESTAMP_SPEC,
dimensionsSpec: {}, dimensionsSpec: {},
granularitySpec: {
rollup: false,
},
}, },
}, },
samplerConfig: BASE_SAMPLER_CONFIG, samplerConfig: BASE_SAMPLER_CONFIG,
@ -367,6 +373,9 @@ export async function sampleForTimestamp(
dataSource: 'sample', dataSource: 'sample',
dimensionsSpec: {}, dimensionsSpec: {},
timestampSpec: timestampSchema === 'column' ? PLACEHOLDER_TIMESTAMP_SPEC : timestampSpec, timestampSpec: timestampSchema === 'column' ? PLACEHOLDER_TIMESTAMP_SPEC : timestampSpec,
granularitySpec: {
rollup: false,
},
}, },
}, },
samplerConfig: BASE_SAMPLER_CONFIG, samplerConfig: BASE_SAMPLER_CONFIG,
@ -396,6 +405,9 @@ export async function sampleForTimestamp(
transformSpec: { transformSpec: {
transforms: transforms.filter(transform => transform.name === TIME_COLUMN), transforms: transforms.filter(transform => transform.name === TIME_COLUMN),
}, },
granularitySpec: {
rollup: false,
},
}, },
}, },
samplerConfig: BASE_SAMPLER_CONFIG, samplerConfig: BASE_SAMPLER_CONFIG,
@ -441,6 +453,9 @@ export async function sampleForTransform(
dataSource: 'sample', dataSource: 'sample',
timestampSpec, timestampSpec,
dimensionsSpec: {}, dimensionsSpec: {},
granularitySpec: {
rollup: false,
},
}, },
}, },
samplerConfig: BASE_SAMPLER_CONFIG, samplerConfig: BASE_SAMPLER_CONFIG,
@ -474,6 +489,9 @@ export async function sampleForTransform(
transformSpec: { transformSpec: {
transforms, transforms,
}, },
granularitySpec: {
rollup: false,
},
}, },
}, },
samplerConfig: BASE_SAMPLER_CONFIG, samplerConfig: BASE_SAMPLER_CONFIG,
@ -502,6 +520,9 @@ export async function sampleForFilter(
dataSource: 'sample', dataSource: 'sample',
timestampSpec, timestampSpec,
dimensionsSpec: {}, dimensionsSpec: {},
granularitySpec: {
rollup: false,
},
}, },
}, },
samplerConfig: BASE_SAMPLER_CONFIG, samplerConfig: BASE_SAMPLER_CONFIG,
@ -536,6 +557,9 @@ export async function sampleForFilter(
transforms, transforms,
filter, filter,
}, },
granularitySpec: {
rollup: false,
},
}, },
}, },
samplerConfig: BASE_SAMPLER_CONFIG, samplerConfig: BASE_SAMPLER_CONFIG,
@ -556,6 +580,7 @@ export async function sampleForSchema(
const metricsSpec: MetricSpec[] = deepGet(spec, 'spec.dataSchema.metricsSpec') || []; const metricsSpec: MetricSpec[] = deepGet(spec, 'spec.dataSchema.metricsSpec') || [];
const queryGranularity: string = const queryGranularity: string =
deepGet(spec, 'spec.dataSchema.granularitySpec.queryGranularity') || 'NONE'; deepGet(spec, 'spec.dataSchema.granularitySpec.queryGranularity') || 'NONE';
const rollup = deepGet(spec, 'spec.dataSchema.granularitySpec.rollup') ?? true;
const sampleSpec: SampleSpec = { const sampleSpec: SampleSpec = {
type: samplerType, type: samplerType,
@ -567,6 +592,7 @@ export async function sampleForSchema(
transformSpec, transformSpec,
granularitySpec: { granularitySpec: {
queryGranularity, queryGranularity,
rollup,
}, },
dimensionsSpec, dimensionsSpec,
metricsSpec, metricsSpec,

View File

@ -0,0 +1,58 @@
/*
* 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 { QueryResult } from 'druid-query-toolkit';
import { filterMap, formatNumber, oneOf } from './general';
import { deepSet } from './object-change';
export interface Pagination {
page: number;
pageSize: number;
}
export function changePage(pagination: Pagination, page: number): Pagination {
return deepSet(pagination, 'page', page);
}
export function getNumericColumnBraces(
queryResult: QueryResult,
pagination?: Pagination,
): Record<number, string[]> {
let rows = queryResult.rows;
if (pagination) {
const index = pagination.page * pagination.pageSize;
rows = rows.slice(index, index + pagination.pageSize);
}
const numericColumnBraces: Record<number, string[]> = {};
if (rows.length) {
queryResult.header.forEach((column, i) => {
if (!oneOf(column.nativeType, 'LONG', 'FLOAT', 'DOUBLE')) return;
const brace = filterMap(rows, row =>
oneOf(typeof row[i], 'number', 'bigint') ? formatNumber(row[i]) : undefined,
);
if (rows.length === brace.length) {
numericColumnBraces[i] = brace;
}
});
}
return numericColumnBraces;
}

View File

@ -1247,7 +1247,13 @@ ORDER BY 1`;
width: 100, width: 100,
Cell: ({ value }) => { Cell: ({ value }) => {
if (isNumberLikeNaN(value)) return '-'; if (isNumberLikeNaN(value)) return '-';
return <BracedText text={formatTotalRows(value)} braces={totalRowsValues} />; return (
<BracedText
text={formatTotalRows(value)}
braces={totalRowsValues}
unselectableThousandsSeparator
/>
);
}, },
}, },
{ {
@ -1258,7 +1264,13 @@ ORDER BY 1`;
width: 100, width: 100,
Cell: ({ value }) => { Cell: ({ value }) => {
if (isNumberLikeNaN(value)) return '-'; if (isNumberLikeNaN(value)) return '-';
return <BracedText text={formatAvgRowSize(value)} braces={avgRowSizeValues} />; return (
<BracedText
text={formatAvgRowSize(value)}
braces={avgRowSizeValues}
unselectableThousandsSeparator
/>
);
}, },
}, },
{ {

View File

@ -7,6 +7,79 @@ exports[`QueryView matches snapshot 1`] = `
<ColumnTree <ColumnTree
columnMetadataLoading={true} columnMetadataLoading={true}
defaultSchema="druid" defaultSchema="druid"
defaultWhere={
SqlComparison {
"decorator": undefined,
"keywords": Object {
"op": ">=",
},
"lhs": SqlRef {
"columnRefName": RefName {
"name": "__time",
"quotes": false,
},
"keywords": Object {},
"namespaceRefName": undefined,
"spacing": Object {},
"tableRefName": undefined,
"type": "ref",
},
"not": false,
"op": ">=",
"rhs": SqlMulti {
"args": SeparatedArray {
"separators": Array [
Separator {
"left": " ",
"right": " ",
"separator": "-",
},
],
"values": Array [
SqlFunction {
"args": undefined,
"decorator": undefined,
"functionName": "CURRENT_TIMESTAMP",
"keywords": Object {
"functionName": "CURRENT_TIMESTAMP",
},
"spacing": Object {},
"specialParen": "none",
"type": "function",
"whereClause": undefined,
},
SqlInterval {
"intervalValue": SqlLiteral {
"keywords": Object {},
"spacing": Object {},
"stringValue": "'1'",
"type": "literal",
"value": "1",
},
"keywords": Object {
"interval": "INTERVAL",
},
"spacing": Object {
"postInterval": " ",
"postIntervalValue": " ",
},
"type": "interval",
"unit": "DAY",
},
],
},
"keywords": Object {},
"op": "-",
"spacing": Object {},
"type": "multi",
},
"spacing": Object {
"postOp": " ",
"preOp": " ",
},
"type": "comparison",
}
}
getParsedQuery={[Function]} getParsedQuery={[Function]}
onQueryChange={[Function]} onQueryChange={[Function]}
/> />
@ -86,6 +159,79 @@ exports[`QueryView matches snapshot with query 1`] = `
<ColumnTree <ColumnTree
columnMetadataLoading={true} columnMetadataLoading={true}
defaultSchema="druid" defaultSchema="druid"
defaultWhere={
SqlComparison {
"decorator": undefined,
"keywords": Object {
"op": ">=",
},
"lhs": SqlRef {
"columnRefName": RefName {
"name": "__time",
"quotes": false,
},
"keywords": Object {},
"namespaceRefName": undefined,
"spacing": Object {},
"tableRefName": undefined,
"type": "ref",
},
"not": false,
"op": ">=",
"rhs": SqlMulti {
"args": SeparatedArray {
"separators": Array [
Separator {
"left": " ",
"right": " ",
"separator": "-",
},
],
"values": Array [
SqlFunction {
"args": undefined,
"decorator": undefined,
"functionName": "CURRENT_TIMESTAMP",
"keywords": Object {
"functionName": "CURRENT_TIMESTAMP",
},
"spacing": Object {},
"specialParen": "none",
"type": "function",
"whereClause": undefined,
},
SqlInterval {
"intervalValue": SqlLiteral {
"keywords": Object {},
"spacing": Object {},
"stringValue": "'1'",
"type": "literal",
"value": "1",
},
"keywords": Object {
"interval": "INTERVAL",
},
"spacing": Object {
"postInterval": " ",
"postIntervalValue": " ",
},
"type": "interval",
"unit": "DAY",
},
],
},
"keywords": Object {},
"op": "-",
"spacing": Object {},
"type": "multi",
},
"spacing": Object {
"postOp": " ",
"preOp": " ",
},
"type": "comparison",
}
}
getParsedQuery={[Function]} getParsedQuery={[Function]}
onQueryChange={[Function]} onQueryChange={[Function]}
/> />

View File

@ -127,6 +127,7 @@ exports[`ColumnTree matches snapshot 1`] = `
</Blueprint3.Popover2>, </Blueprint3.Popover2>,
}, },
], ],
"className": undefined,
"icon": "th", "icon": "th",
"id": "wikipedia", "id": "wikipedia",
"isExpanded": true, "isExpanded": true,

View File

@ -42,17 +42,12 @@ const BETWEEN: SqlExpression = SqlExpression.parse(`(? <= ? AND ? < ?)`);
// ------------------------------------ // ------------------------------------
function fillWithColumn(b: SqlExpression, columnName: string): SqlExpression { function fillWithColumn(b: SqlExpression, columnName: string): SqlExpression {
return b.fillPlaceholders([SqlRef.column(columnName)]) as SqlExpression; return b.fillPlaceholders([SqlRef.column(columnName)]);
} }
function fillWithColumnStartEnd(columnName: string, start: Date, end: Date): SqlExpression { function fillWithColumnStartEnd(columnName: string, start: Date, end: Date): SqlExpression {
const ref = SqlRef.column(columnName); const ref = SqlRef.column(columnName);
return BETWEEN.fillPlaceholders([ return BETWEEN.fillPlaceholders([SqlLiteral.create(start), ref, ref, SqlLiteral.create(end)])!;
SqlLiteral.create(start),
ref,
ref,
SqlLiteral.create(end),
]) as SqlExpression;
} }
// ------------------------------------ // ------------------------------------

View File

@ -16,8 +16,18 @@
* limitations under the License. * limitations under the License.
*/ */
@import '../../../variables';
@import '../../../blueprint-overrides/common/colors'; @import '../../../blueprint-overrides/common/colors';
@keyframes druid-glow {
0% {
text-shadow: 0 0 0 $druid-brand2;
}
100% {
text-shadow: 0 0 2px $druid-brand2;
}
}
.column-tree { .column-tree {
.bp3-dark & { .bp3-dark & {
background: $dark-gray3; background: $dark-gray3;
@ -43,6 +53,10 @@
.bp3-tree-node-content-1 { .bp3-tree-node-content-1 {
padding-left: 0; padding-left: 0;
} }
.highlight {
animation: druid-glow 1s infinite alternate;
}
} }
.bp3-popover2-target { .bp3-popover2-target {

View File

@ -39,9 +39,12 @@ import { NumberMenuItems, StringMenuItems, TimeMenuItems } from './column-tree-m
import './column-tree.scss'; import './column-tree.scss';
const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
const COUNT_STAR = SqlFunction.COUNT_STAR.as('Count'); const COUNT_STAR = SqlFunction.COUNT_STAR.as('Count');
function caseInsensitiveCompare(a: any, b: any): number {
return String(a).toLowerCase().localeCompare(String(b).toLowerCase());
}
function getCountExpression(columnNames: string[]): SqlExpression { function getCountExpression(columnNames: string[]): SqlExpression {
for (const columnName of columnNames) { for (const columnName of columnNames) {
if (columnName === 'count' || columnName === '__count') { if (columnName === 'count' || columnName === '__count') {
@ -69,11 +72,20 @@ interface HandleColumnClickOptions {
columnName: string; columnName: string;
columnType: string; columnType: string;
parsedQuery: SqlQuery | undefined; parsedQuery: SqlQuery | undefined;
defaultWhere: SqlExpression | undefined;
onQueryChange: (query: SqlQuery, run: boolean) => void; onQueryChange: (query: SqlQuery, run: boolean) => void;
} }
function handleColumnShow(options: HandleColumnClickOptions): void { function handleColumnShow(options: HandleColumnClickOptions): void {
const { columnSchema, columnTable, columnName, columnType, parsedQuery, onQueryChange } = options; const {
columnSchema,
columnTable,
columnName,
columnType,
parsedQuery,
defaultWhere,
onQueryChange,
} = options;
let from: SqlExpression; let from: SqlExpression;
let where: SqlExpression | undefined; let where: SqlExpression | undefined;
@ -84,7 +96,7 @@ function handleColumnShow(options: HandleColumnClickOptions): void {
aggregates = parsedQuery.getAggregateSelectExpressions(); aggregates = parsedQuery.getAggregateSelectExpressions();
} else if (columnSchema === 'druid') { } else if (columnSchema === 'druid') {
from = SqlTableRef.create(columnTable); from = SqlTableRef.create(columnTable);
where = LAST_DAY; where = defaultWhere;
} else { } else {
from = SqlTableRef.create(columnTable, columnSchema); from = SqlTableRef.create(columnTable, columnSchema);
} }
@ -118,9 +130,11 @@ export interface ColumnTreeProps {
columnMetadataLoading: boolean; columnMetadataLoading: boolean;
columnMetadata?: readonly ColumnMetadata[]; columnMetadata?: readonly ColumnMetadata[];
getParsedQuery: () => SqlQuery | undefined; getParsedQuery: () => SqlQuery | undefined;
defaultWhere?: SqlExpression;
onQueryChange: (query: SqlQuery, run?: boolean) => void; onQueryChange: (query: SqlQuery, run?: boolean) => void;
defaultSchema?: string; defaultSchema?: string;
defaultTable?: string; defaultTable?: string;
highlightTable?: string;
} }
export interface ColumnTreeState { export interface ColumnTreeState {
@ -154,7 +168,14 @@ export function getJoinColumns(parsedQuery: SqlQuery, _table: string) {
export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeState> { export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeState> {
static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) { static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) {
const { columnMetadata, defaultSchema, defaultTable, onQueryChange } = props; const {
columnMetadata,
defaultSchema,
defaultTable,
defaultWhere,
onQueryChange,
highlightTable,
} = props;
if (columnMetadata && columnMetadata !== state.prevColumnMetadata) { if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
const columnTree = groupBy( const columnTree = groupBy(
@ -169,6 +190,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
(metadata, tableName): TreeNodeInfo => ({ (metadata, tableName): TreeNodeInfo => ({
id: tableName, id: tableName,
icon: IconNames.TH, icon: IconNames.TH,
className: tableName === highlightTable ? 'highlight' : undefined,
label: ( label: (
<Popover2 <Popover2
position={Position.RIGHT} position={Position.RIGHT}
@ -195,7 +217,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
if (parsedQuery && parsedQuery.getFirstTableName() === tableName) { if (parsedQuery && parsedQuery.getFirstTableName() === tableName) {
return parsedQuery.getWhereExpression(); return parsedQuery.getWhereExpression();
} else if (schemaName === 'druid') { } else if (schemaName === 'druid') {
return defaultToAllTime ? undefined : LAST_DAY; return defaultToAllTime ? undefined : defaultWhere;
} else { } else {
return; return;
} }
@ -210,7 +232,10 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
onQueryChange( onQueryChange(
getQueryOnTable() getQueryOnTable()
.changeSelectExpressions( .changeSelectExpressions(
metadata.map(child => SqlRef.column(child.COLUMN_NAME)), metadata
.map(child => child.COLUMN_NAME)
.sort(caseInsensitiveCompare)
.map(columnName => SqlRef.column(columnName)),
) )
.changeWhereExpression(getWhere()), .changeWhereExpression(getWhere()),
true, true,
@ -376,6 +401,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
columnName: columnData.COLUMN_NAME, columnName: columnData.COLUMN_NAME,
columnType: columnData.DATA_TYPE, columnType: columnData.DATA_TYPE,
parsedQuery, parsedQuery,
defaultWhere,
onQueryChange: onQueryChange, onQueryChange: onQueryChange,
}); });
}} }}
@ -429,9 +455,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
), ),
}), }),
) )
.sort((a, b) => .sort((a, b) => caseInsensitiveCompare(a.id, b.id)),
String(a.id).toLowerCase().localeCompare(String(b.id).toLowerCase()),
),
}), }),
), ),
}), }),

View File

@ -20,6 +20,8 @@
@import '../../../blueprint-overrides/common/colors'; @import '../../../blueprint-overrides/common/colors';
.query-input { .query-input {
position: relative;
.ace-container { .ace-container {
position: absolute; position: absolute;
width: 100%; width: 100%;

View File

@ -31,13 +31,14 @@ import {
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { BracedText, TableCell } from '../../../components'; import { BracedText, Deferred, TableCell } from '../../../components';
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog'; import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
import { import {
changePage,
copyAndAlert, copyAndAlert,
deepSet, formatNumber,
filterMap, getNumericColumnBraces,
oneOf, Pagination,
prettyPrintSql, prettyPrintSql,
SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE,
SMALL_TABLE_PAGE_SIZE_OPTIONS, SMALL_TABLE_PAGE_SIZE_OPTIONS,
@ -53,38 +54,6 @@ function isComparable(x: unknown): boolean {
return x !== null && x !== '' && !isNaN(Number(x)); return x !== null && x !== '' && !isNaN(Number(x));
} }
interface Pagination {
page: number;
pageSize: number;
}
function changePage(pagination: Pagination, page: number): Pagination {
return deepSet(pagination, 'page', page);
}
function getNumericColumnBraces(
queryResult: QueryResult,
pagination: Pagination,
): Record<number, string[]> {
const numericColumnBraces: Record<number, string[]> = {};
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 =>
oneOf(typeof row[c], 'number', 'bigint') ? String(row[c]) : undefined,
);
if (rows.length === brace.length) {
numericColumnBraces[c] = brace;
}
}
}
return numericColumnBraces;
}
export interface QueryOutputProps { export interface QueryOutputProps {
queryResult: QueryResult; queryResult: QueryResult;
onQueryChange: (query: SqlQuery, run?: boolean) => void; onQueryChange: (query: SqlQuery, run?: boolean) => void;
@ -123,8 +92,8 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
const orderByExpression = parsedQuery.isValidSelectIndex(headerIndex) const orderByExpression = parsedQuery.isValidSelectIndex(headerIndex)
? SqlLiteral.index(headerIndex) ? SqlLiteral.index(headerIndex)
: SqlRef.column(header); : SqlRef.column(header);
const descOrderBy = orderByExpression.toOrderByPart('DESC'); const descOrderBy = orderByExpression.toOrderByExpression('DESC');
const ascOrderBy = orderByExpression.toOrderByPart('ASC'); const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
const orderBy = parsedQuery.getOrderByForSelectIndex(headerIndex); const orderBy = parsedQuery.getOrderByForSelectIndex(headerIndex);
const basicActions: BasicAction[] = []; const basicActions: BasicAction[] = [];
@ -205,11 +174,11 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
}, },
}); });
return basicActionsToMenu(basicActions); return basicActionsToMenu(basicActions)!;
} else { } else {
const orderByExpression = SqlRef.column(header); const orderByExpression = SqlRef.column(header);
const descOrderBy = orderByExpression.toOrderByPart('DESC'); const descOrderBy = orderByExpression.toOrderByExpression('DESC');
const ascOrderBy = orderByExpression.toOrderByPart('ASC'); const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
const descOrderByPretty = prettyPrintSql(descOrderBy); const descOrderByPretty = prettyPrintSql(descOrderBy);
const ascOrderByPretty = prettyPrintSql(descOrderBy); const ascOrderByPretty = prettyPrintSql(descOrderBy);
return ( return (
@ -409,7 +378,10 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
? () => <ColumnRenameInput initialName={h} onDone={renameColumnTo} /> ? () => <ColumnRenameInput initialName={h} onDone={renameColumnTo} />
: () => { : () => {
return ( return (
<Popover2 className="clickable-cell" content={getHeaderMenu(h, i)}> <Popover2
className="clickable-cell"
content={<Deferred content={() => getHeaderMenu(h, i)} />}
>
<div> <div>
{h} {h}
{hasFilterOnHeader(h, i) && ( {hasFilterOnHeader(h, i) && (
@ -421,16 +393,17 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
}, },
headerClassName: getHeaderClassName(h, i), headerClassName: getHeaderClassName(h, i),
accessor: String(i), accessor: String(i),
Cell: function QueryOutputTableCell(row) { Cell(row) {
const value = row.value; const value = row.value;
return ( return (
<div> <div>
<Popover2 content={getCellMenu(h, i, value)}> <Popover2 content={<Deferred content={() => getCellMenu(h, i, value)} />}>
{numericColumnBraces[i] ? ( {numericColumnBraces[i] ? (
<BracedText <BracedText
text={String(value)} text={formatNumber(value)}
braces={numericColumnBraces[i]} braces={numericColumnBraces[i]}
padFractionalPart padFractionalPart
unselectableThousandsSeparator
/> />
) : ( ) : (
<TableCell value={value} unlimited /> <TableCell value={value} unlimited />

View File

@ -20,12 +20,19 @@ import { IconName } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
export function dataTypeToIcon(dataType: string): IconName { export function dataTypeToIcon(dataType: string): IconName {
switch (dataType) { const typeUpper = dataType.toUpperCase();
if (typeUpper.startsWith('COMPLEX')) {
return IconNames.ASTERISK;
}
switch (typeUpper) {
case 'TIMESTAMP': case 'TIMESTAMP':
return IconNames.TIME; return IconNames.TIME;
case 'VARCHAR': case 'VARCHAR':
case 'STRING':
return IconNames.FONT; return IconNames.FONT;
case 'BIGINT': case 'BIGINT':
case 'LONG':
case 'FLOAT': case 'FLOAT':
case 'DOUBLE': case 'DOUBLE':
return IconNames.NUMERICAL; return IconNames.NUMERICAL;

View File

@ -19,7 +19,7 @@
import { Code, Intent, Switch } from '@blueprintjs/core'; import { Code, Intent, Switch } from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2'; import { Tooltip2 } from '@blueprintjs/popover2';
import classNames from 'classnames'; import classNames from 'classnames';
import { QueryResult, QueryRunner, SqlQuery } from 'druid-query-toolkit'; import { QueryResult, QueryRunner, SqlExpression, SqlQuery } from 'druid-query-toolkit';
import Hjson from 'hjson'; import Hjson from 'hjson';
import * as JSONBig from 'json-bigint-native'; import * as JSONBig from 'json-bigint-native';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
@ -67,6 +67,8 @@ import { RunButton } from './run-button/run-button';
import './query-view.scss'; import './query-view.scss';
const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
const parser = memoizeOne((sql: string): SqlQuery | undefined => { const parser = memoizeOne((sql: string): SqlQuery | undefined => {
try { try {
return SqlQuery.parse(sql); return SqlQuery.parse(sql);
@ -193,9 +195,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
}, },
}); });
const queryRunner = new QueryRunner((payload, isSql, cancelToken) => { const queryRunner = new QueryRunner();
return Api.instance.post(`/druid/v2${isSql ? '/sql' : ''}`, payload, { cancelToken });
});
this.queryManager = new QueryManager({ this.queryManager = new QueryManager({
processQuery: async ( processQuery: async (
@ -627,6 +627,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
getParsedQuery={this.getParsedQuery} getParsedQuery={this.getParsedQuery}
columnMetadataLoading={columnMetadataState.loading} columnMetadataLoading={columnMetadataState.loading}
columnMetadata={columnMetadataState.data} columnMetadata={columnMetadataState.data}
defaultWhere={LAST_DAY}
onQueryChange={this.handleQueryChange} onQueryChange={this.handleQueryChange}
defaultSchema={defaultSchema ? defaultSchema : 'druid'} defaultSchema={defaultSchema ? defaultSchema : 'druid'}
defaultTable={defaultTable} defaultTable={defaultTable}

View File

@ -369,7 +369,7 @@ exports[`segments-view matches snapshot 1`] = `
rowsText="rows" rowsText="rows"
showPageJump={false} showPageJump={false}
showPageSizeOptions={true} showPageSizeOptions={true}
showPagination={false} showPagination={true}
showPaginationBottom={true} showPaginationBottom={true}
showPaginationTop={false} showPaginationTop={false}
sortable={true} sortable={true}

View File

@ -173,8 +173,8 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
const columns = compact([ const columns = compact([
visibleColumns.shown('Segment ID') && `"segment_id"`, visibleColumns.shown('Segment ID') && `"segment_id"`,
visibleColumns.shown('Datasource') && `"datasource"`, visibleColumns.shown('Datasource') && `"datasource"`,
visibleColumns.shown('Start') && `"start"`, `"start"`,
visibleColumns.shown('End') && `"end"`, `"end"`,
visibleColumns.shown('Version') && `"version"`, visibleColumns.shown('Version') && `"version"`,
visibleColumns.shown('Time span') && visibleColumns.shown('Time span') &&
`CASE `CASE
@ -191,6 +191,7 @@ END AS "time_span"`,
WHEN "shard_spec" LIKE '%"type":"numbered"%' THEN 'dynamic' WHEN "shard_spec" LIKE '%"type":"numbered"%' THEN 'dynamic'
WHEN "shard_spec" LIKE '%"type":"hashed"%' THEN 'hashed' WHEN "shard_spec" LIKE '%"type":"hashed"%' THEN 'hashed'
WHEN "shard_spec" LIKE '%"type":"single"%' THEN 'single_dim' WHEN "shard_spec" LIKE '%"type":"single"%' THEN 'single_dim'
WHEN "shard_spec" LIKE '%"type":"range"%' THEN 'range'
WHEN "shard_spec" LIKE '%"type":"none"%' THEN 'none' WHEN "shard_spec" LIKE '%"type":"none"%' THEN 'none'
WHEN "shard_spec" LIKE '%"type":"linear"%' THEN 'linear' WHEN "shard_spec" LIKE '%"type":"linear"%' THEN 'linear'
WHEN "shard_spec" LIKE '%"type":"numbered_overwrite"%' THEN 'numbered_overwrite' WHEN "shard_spec" LIKE '%"type":"numbered_overwrite"%' THEN 'numbered_overwrite'
@ -206,10 +207,6 @@ END AS "partitioning"`,
visibleColumns.shown('Is overshadowed') && `"is_overshadowed"`, visibleColumns.shown('Is overshadowed') && `"is_overshadowed"`,
]); ]);
if (!columns.length) {
columns.push(`"segment_id"`);
}
return `WITH s AS (SELECT\n${columns.join(',\n')}\nFROM sys.segments)`; return `WITH s AS (SELECT\n${columns.join(',\n')}\nFROM sys.segments)`;
} }
@ -516,7 +513,7 @@ END AS "partitioning"`,
pivotBy={groupByInterval ? ['interval'] : []} pivotBy={groupByInterval ? ['interval'] : []}
defaultPageSize={STANDARD_TABLE_PAGE_SIZE} defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS} pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
showPagination={segments.length > STANDARD_TABLE_PAGE_SIZE} showPagination
columns={[ columns={[
{ {
Header: 'Segment ID', Header: 'Segment ID',
@ -625,6 +622,7 @@ END AS "partitioning"`,
<BracedText <BracedText
text={row.original.is_available ? formatInteger(row.value) : '(unknown)'} text={row.original.is_available ? formatInteger(row.value) : '(unknown)'}
braces={numRowsValues} braces={numRowsValues}
unselectableThousandsSeparator
/> />
), ),
}, },

View File

@ -183,7 +183,7 @@ exports[`ServicesView renders data 1`] = `
"filterable": false, "filterable": false,
"id": "usage", "id": "usage",
"show": true, "show": true,
"width": 100, "width": 140,
}, },
Object { Object {
"Aggregated": [Function], "Aggregated": [Function],

View File

@ -444,7 +444,7 @@ ORDER BY "rank" DESC, "service" DESC`;
Header: 'Usage', Header: 'Usage',
show: visibleColumns.shown('Usage'), show: visibleColumns.shown('Usage'),
id: 'usage', id: 'usage',
width: 100, width: 140,
filterable: false, filterable: false,
accessor: row => { accessor: row => {
if (oneOf(row.service_type, 'middle_manager', 'indexer')) { if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
@ -496,9 +496,9 @@ ORDER BY "rank" DESC, "service" DESC`;
const currCapacityUsed = deepGet(row, 'original.currCapacityUsed') || 0; const currCapacityUsed = deepGet(row, 'original.currCapacityUsed') || 0;
const capacity = deepGet(row, 'original.worker.capacity'); const capacity = deepGet(row, 'original.worker.capacity');
if (typeof capacity === 'number') { if (typeof capacity === 'number') {
return `${currCapacityUsed} / ${capacity} (slots)`; return `Slots used: ${currCapacityUsed} of ${capacity}`;
} else { } else {
return '- / -'; return 'Slots used: -';
} }
} }