mirror of https://github.com/apache/druid.git
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:
parent
0b3f0bbbd8
commit
1d3c8c187b
|
@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
@ -53,6 +53,7 @@ exports.SQL_KEYWORDS = [
|
||||||
'ROW',
|
'ROW',
|
||||||
'ROWS',
|
'ROWS',
|
||||||
'ONLY',
|
'ONLY',
|
||||||
|
'VALUES',
|
||||||
];
|
];
|
||||||
|
|
||||||
exports.SQL_EXPRESSION_PARTS = [
|
exports.SQL_EXPRESSION_PARTS = [
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -38,5 +38,9 @@
|
||||||
.zero-pad {
|
.zero-pad {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unselectable {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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: -';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue