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
|
||||
license_name: MIT License
|
||||
copyright: Matt Zabriskie
|
||||
version: 0.21.1
|
||||
version: 0.21.4
|
||||
license_file_path: licenses/bin/axios.MIT
|
||||
|
||||
---
|
||||
|
@ -5294,7 +5294,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: Apache License version 2.0
|
||||
copyright: Imply Data
|
||||
version: 0.11.10
|
||||
version: 0.14.4
|
||||
|
||||
---
|
||||
|
||||
|
@ -5313,7 +5313,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: MIT License
|
||||
copyright: Ruben Verborgh
|
||||
version: 1.13.3
|
||||
version: 1.14.4
|
||||
license_file_path: licenses/bin/follow-redirects.MIT
|
||||
|
||||
---
|
||||
|
|
|
@ -53,6 +53,7 @@ exports.SQL_KEYWORDS = [
|
|||
'ROW',
|
||||
'ROWS',
|
||||
'ONLY',
|
||||
'VALUES',
|
||||
];
|
||||
|
||||
exports.SQL_EXPRESSION_PARTS = [
|
||||
|
|
|
@ -7970,9 +7970,9 @@
|
|||
}
|
||||
},
|
||||
"druid-query-toolkit": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.11.10.tgz",
|
||||
"integrity": "sha512-jKqec2YMxCVvow8e9lmmrRKXxq/ugyeyKTVPaAUPbjoP4VHxk55BS2gXJ/S2ysCeVgvyJbjGbg2ZIkUzg4Whuw==",
|
||||
"version": "0.14.4",
|
||||
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.14.4.tgz",
|
||||
"integrity": "sha512-PmD5vwoHQxNxZ8E8vRdHvh5OjuvA+yHD5dhiKDzIzPtnFiwRHLJKyOLSQ6rmN1VAKbOdU4JCZIzPFUB8bEMBAQ==",
|
||||
"requires": {
|
||||
"tslib": "^2.2.0"
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
"d3-axis": "^1.0.12",
|
||||
"d3-scale": "^3.2.0",
|
||||
"d3-selection": "^1.4.0",
|
||||
"druid-query-toolkit": "^0.11.10",
|
||||
"druid-query-toolkit": "^0.14.4",
|
||||
"file-saver": "^2.0.2",
|
||||
"fontsource-open-sans": "^3.0.9",
|
||||
"has-own-prop": "^2.0.0",
|
||||
|
|
|
@ -38,5 +38,9 @@
|
|||
.zero-pad {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.unselectable {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,14 +17,17 @@
|
|||
*/
|
||||
|
||||
import { max } from 'd3-array';
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import './braced-text.scss';
|
||||
|
||||
const THOUSANDS_SEPARATOR = ','; // Maybe one day make this locale aware
|
||||
|
||||
export interface BracedTextProps {
|
||||
text: string;
|
||||
braces: string[];
|
||||
padFractionalPart?: boolean;
|
||||
unselectableThousandsSeparator?: boolean;
|
||||
}
|
||||
|
||||
export function findMostNumbers(strings: string[]): string {
|
||||
|
@ -56,8 +59,29 @@ function zerosOfLength(n: number): string {
|
|||
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) {
|
||||
const { text, braces, padFractionalPart } = props;
|
||||
const { text, braces, padFractionalPart, unselectableThousandsSeparator } = props;
|
||||
|
||||
let effectiveBraces = braces.concat(text);
|
||||
|
||||
|
@ -90,7 +114,7 @@ export const BracedText = React.memo(function BracedText(props: BracedTextProps)
|
|||
<span className="braced-text">
|
||||
<span className="brace-text">{findMostNumbers(effectiveBraces)}</span>
|
||||
<span className="real-text">
|
||||
{text}
|
||||
{unselectableThousandsSeparator ? hideThousandsSeparator(text) : text}
|
||||
{zeroPad}
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
|
||||
export interface DeferredProps {
|
||||
content: () => JSX.Element;
|
||||
content: () => JSX.Element | null;
|
||||
}
|
||||
|
||||
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 './center-message/center-message';
|
||||
export * from './clearable-input/clearable-input';
|
||||
export * from './deferred/deferred';
|
||||
export * from './external-link/external-link';
|
||||
export * from './form-json-selector/form-json-selector';
|
||||
export * from './formatted-input/formatted-input';
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import './loader.scss';
|
||||
|
||||
export interface LoaderProps {
|
||||
className?: string;
|
||||
loading?: boolean; // This is needed so that this component can be used as a LoadingComponent in react table
|
||||
loadingText?: string;
|
||||
cancelText?: string;
|
||||
|
@ -28,11 +30,11 @@ export interface 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;
|
||||
|
||||
return (
|
||||
<div className="loader">
|
||||
<div className={classNames('loader', className)}>
|
||||
<div className="loader-logo">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<path
|
||||
|
|
|
@ -16,26 +16,26 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { MenuItem, MenuItemProps } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
export interface MenuCheckboxProps {
|
||||
text: string;
|
||||
export interface MenuCheckboxProps extends Omit<MenuItemProps, 'icon' | 'onClick'> {
|
||||
checked: boolean;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export function MenuCheckbox(props: MenuCheckboxProps) {
|
||||
const { text, checked, onChange } = props;
|
||||
const { checked, onChange, className, shouldDismissPopover, ...rest } = props;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className="menu-checkbox"
|
||||
className={classNames('menu-checkbox', className)}
|
||||
icon={checked ? IconNames.TICK_CIRCLE : IconNames.CIRCLE}
|
||||
text={text}
|
||||
onClick={onChange}
|
||||
shouldDismissPopover={false}
|
||||
shouldDismissPopover={shouldDismissPopover ?? false}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
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 { Capabilities } from '../../utils';
|
||||
|
|
|
@ -207,6 +207,15 @@ exports[`CompactionDialog matches snapshot with compactionConfig (dynamic partit
|
|||
"required": true,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"defined": [Function],
|
||||
"info": <p>
|
||||
The dimensions to partition on.
|
||||
</p>,
|
||||
"name": "tuningConfig.partitionsSpec.partitionDimensions",
|
||||
"required": true,
|
||||
"type": "string-array",
|
||||
},
|
||||
Object {
|
||||
"defined": [Function],
|
||||
"info": <React.Fragment>
|
||||
|
@ -568,6 +577,15 @@ exports[`CompactionDialog matches snapshot with compactionConfig (hashed partiti
|
|||
"required": true,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"defined": [Function],
|
||||
"info": <p>
|
||||
The dimensions to partition on.
|
||||
</p>,
|
||||
"name": "tuningConfig.partitionsSpec.partitionDimensions",
|
||||
"required": true,
|
||||
"type": "string-array",
|
||||
},
|
||||
Object {
|
||||
"defined": [Function],
|
||||
"info": <React.Fragment>
|
||||
|
@ -929,6 +947,15 @@ exports[`CompactionDialog matches snapshot with compactionConfig (single_dim par
|
|||
"required": true,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"defined": [Function],
|
||||
"info": <p>
|
||||
The dimensions to partition on.
|
||||
</p>,
|
||||
"name": "tuningConfig.partitionsSpec.partitionDimensions",
|
||||
"required": true,
|
||||
"type": "string-array",
|
||||
},
|
||||
Object {
|
||||
"defined": [Function],
|
||||
"info": <React.Fragment>
|
||||
|
@ -1290,6 +1317,15 @@ exports[`CompactionDialog matches snapshot without compactionConfig 1`] = `
|
|||
"required": true,
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"defined": [Function],
|
||||
"info": <p>
|
||||
The dimensions to partition on.
|
||||
</p>,
|
||||
"name": "tuningConfig.partitionsSpec.partitionDimensions",
|
||||
"required": true,
|
||||
"type": "string-array",
|
||||
},
|
||||
Object {
|
||||
"defined": [Function],
|
||||
"info": <React.Fragment>
|
||||
|
|
|
@ -16,7 +16,7 @@ exports[`clipboard dialog matches snapshot 1`] = `
|
|||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog show-value-dialog"
|
||||
class="bp3-dialog show-value-dialog normal"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog-header"
|
||||
|
@ -54,6 +54,7 @@ exports[`clipboard dialog matches snapshot 1`] = `
|
|||
</div>
|
||||
<textarea
|
||||
class="bp3-input"
|
||||
spellcheck="false"
|
||||
>
|
||||
Bot: Automatska zamjena teksta (-[[Administrativna podjela Meksika|Admin]] +[[Administrativna podjela Meksika|Admi]])
|
||||
</textarea>
|
||||
|
|
|
@ -21,9 +21,18 @@
|
|||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
&.normal.bp3-dialog {
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
&.large.bp3-dialog {
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.bp3-input {
|
||||
margin: 10px;
|
||||
height: 400px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bp3-dialog-footer-actions {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import { Button, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import classNames from 'classnames';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -28,10 +29,11 @@ import './show-value-dialog.scss';
|
|||
export interface ShowValueDialogProps {
|
||||
onClose: () => void;
|
||||
str: string;
|
||||
size?: 'normal' | 'large';
|
||||
}
|
||||
|
||||
export const ShowValueDialog = React.memo(function ShowValueDialog(props: ShowValueDialogProps) {
|
||||
const { onClose, str } = props;
|
||||
const { onClose, str, size } = props;
|
||||
|
||||
function handleCopy() {
|
||||
copy(str, { format: 'text/plain' });
|
||||
|
@ -42,8 +44,13 @@ export const ShowValueDialog = React.memo(function ShowValueDialog(props: ShowVa
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog className="show-value-dialog" isOpen onClose={onClose} title="Full value">
|
||||
<TextArea value={str} />
|
||||
<Dialog
|
||||
className={classNames('show-value-dialog', size || 'normal')}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
title="Full value"
|
||||
>
|
||||
<TextArea value={str} spellCheck={false} />
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button icon={IconNames.DUPLICATE} text="Copy" onClick={handleCopy} />
|
||||
<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',
|
||||
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',
|
||||
type: 'string',
|
||||
|
@ -168,12 +168,19 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
|
|||
required: true,
|
||||
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',
|
||||
type: 'number',
|
||||
zeroMeansUndefined: true,
|
||||
defined: t =>
|
||||
deepGet(t, 'tuningConfig.partitionsSpec.type') === 'single_dim' &&
|
||||
oneOf(deepGet(t, 'tuningConfig.partitionsSpec.type'), 'single_dim', 'range') &&
|
||||
!deepGet(t, 'tuningConfig.partitionsSpec.maxRowsPerSegment'),
|
||||
required: (t: CompactionConfig) =>
|
||||
!deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment') &&
|
||||
|
@ -196,7 +203,7 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
|
|||
type: 'number',
|
||||
zeroMeansUndefined: true,
|
||||
defined: t =>
|
||||
deepGet(t, 'tuningConfig.partitionsSpec.type') === 'single_dim' &&
|
||||
oneOf(deepGet(t, 'tuningConfig.partitionsSpec.type'), 'single_dim', 'range') &&
|
||||
!deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'),
|
||||
required: (t: CompactionConfig) =>
|
||||
!deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment') &&
|
||||
|
@ -215,7 +222,7 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
|
|||
name: 'tuningConfig.partitionsSpec.assumeGrouped',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
defined: t => deepGet(t, 'tuningConfig.partitionsSpec.type') === 'single_dim',
|
||||
defined: t => oneOf(deepGet(t, 'tuningConfig.partitionsSpec.type'), 'single_dim', 'range'),
|
||||
info: (
|
||||
<p>
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
const ioConfig = deepGet(spec, 'spec.ioConfig');
|
||||
if (!ioConfig) return;
|
||||
|
@ -1239,39 +1273,7 @@ export function guessDataSourceName(spec: Partial<IngestionSpec>): string | unde
|
|||
case 'index_parallel': {
|
||||
const inputSource = ioConfig.inputSource;
|
||||
if (!inputSource) return;
|
||||
|
||||
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;
|
||||
return guessDataSourceNameFromInputSource(inputSource);
|
||||
}
|
||||
|
||||
case 'kafka':
|
||||
|
@ -1340,7 +1342,7 @@ export function adjustForceGuaranteedRollup(spec: Partial<IngestionSpec>) {
|
|||
const partitionsSpecType = deepGet(spec, 'spec.tuningConfig.partitionsSpec.type') || 'dynamic';
|
||||
if (partitionsSpecType === 'dynamic') {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
type: 'number',
|
||||
zeroMeansUndefined: true,
|
||||
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'),
|
||||
required: s =>
|
||||
!deepGet(s, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment') &&
|
||||
|
@ -1545,7 +1554,7 @@ export function getSecondaryPartitionRelatedFormFields(
|
|||
type: 'number',
|
||||
zeroMeansUndefined: true,
|
||||
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'),
|
||||
required: s =>
|
||||
!deepGet(s, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment') &&
|
||||
|
@ -1557,7 +1566,8 @@ export function getSecondaryPartitionRelatedFormFields(
|
|||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
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: (
|
||||
<p>
|
||||
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,
|
||||
min: 1,
|
||||
defined: s =>
|
||||
Boolean(
|
||||
s.type === 'index_parallel' &&
|
||||
oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'hashed', 'single_dim'),
|
||||
),
|
||||
s.type === 'index_parallel' &&
|
||||
oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'hashed', 'single_dim', 'range'),
|
||||
info: <>Number of tasks to merge partial segments after shuffle.</>,
|
||||
},
|
||||
{
|
||||
|
@ -1638,10 +1646,8 @@ const TUNING_FORM_FIELDS: Field<IngestionSpec>[] = [
|
|||
type: 'number',
|
||||
defaultValue: 100,
|
||||
defined: s =>
|
||||
Boolean(
|
||||
s.type === 'index_parallel' &&
|
||||
oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'hashed', 'single_dim'),
|
||||
),
|
||||
s.type === 'index_parallel' &&
|
||||
oneOf(deepGet(s, 'spec.tuningConfig.partitionsSpec.type'), 'hashed', 'single_dim', 'range'),
|
||||
info: (
|
||||
<>
|
||||
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 './bootstrap/ace';
|
||||
|
||||
import { QueryRunner } from 'druid-query-toolkit';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
|
@ -88,6 +89,10 @@ if (consoleConfig.linkOverrides) {
|
|||
setLinkOverrides(consoleConfig.linkOverrides);
|
||||
}
|
||||
|
||||
QueryRunner.defaultQueryExecutor = (payload, isSql, cancelToken) => {
|
||||
return Api.instance.post(`/druid/v2${isSql ? '/sql' : ''}`, payload, { cancelToken });
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(ConsoleApplication, {
|
||||
exampleManifestsUrl: consoleConfig.exampleManifestsUrl,
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* 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';
|
||||
|
||||
|
|
|
@ -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>(
|
||||
|
@ -246,6 +254,10 @@ export function formatInteger(n: NumberLike): string {
|
|||
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 {
|
||||
return numeral(n).format('0.00 b');
|
||||
}
|
||||
|
@ -262,6 +274,10 @@ export function formatPercent(n: NumberLike): string {
|
|||
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 {
|
||||
const s = (Number(n) / 1e6).toFixed(3);
|
||||
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);
|
||||
}
|
||||
|
||||
function pad3(str: string | number): string {
|
||||
return ('000' + str).substr(-3);
|
||||
}
|
||||
|
||||
export function formatDuration(ms: NumberLike): string {
|
||||
const n = Number(ms);
|
||||
const timeInHours = Math.floor(n / 3600000);
|
||||
|
@ -280,6 +300,16 @@ export function formatDuration(ms: NumberLike): string {
|
|||
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 {
|
||||
if (!plural) plural = singular + 's';
|
||||
return `${formatInteger(n)} ${n === 1 ? singular : plural}`;
|
||||
|
|
|
@ -30,3 +30,4 @@ export * from './query-cursor';
|
|||
export * from './query-manager';
|
||||
export * from './query-state';
|
||||
export * from './sanitizers';
|
||||
export * from './table-helpers';
|
||||
|
|
|
@ -286,6 +286,9 @@ export async function sampleForConnect(
|
|||
dataSource: 'sample',
|
||||
timestampSpec: reingestMode ? REINDEX_TIMESTAMP_SPEC : PLACEHOLDER_TIMESTAMP_SPEC,
|
||||
dimensionsSpec: {},
|
||||
granularitySpec: {
|
||||
rollup: false,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
samplerConfig: BASE_SAMPLER_CONFIG,
|
||||
|
@ -342,6 +345,9 @@ export async function sampleForParser(
|
|||
dataSource: 'sample',
|
||||
timestampSpec: reingestMode ? REINDEX_TIMESTAMP_SPEC : PLACEHOLDER_TIMESTAMP_SPEC,
|
||||
dimensionsSpec: {},
|
||||
granularitySpec: {
|
||||
rollup: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
samplerConfig: BASE_SAMPLER_CONFIG,
|
||||
|
@ -367,6 +373,9 @@ export async function sampleForTimestamp(
|
|||
dataSource: 'sample',
|
||||
dimensionsSpec: {},
|
||||
timestampSpec: timestampSchema === 'column' ? PLACEHOLDER_TIMESTAMP_SPEC : timestampSpec,
|
||||
granularitySpec: {
|
||||
rollup: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
samplerConfig: BASE_SAMPLER_CONFIG,
|
||||
|
@ -396,6 +405,9 @@ export async function sampleForTimestamp(
|
|||
transformSpec: {
|
||||
transforms: transforms.filter(transform => transform.name === TIME_COLUMN),
|
||||
},
|
||||
granularitySpec: {
|
||||
rollup: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
samplerConfig: BASE_SAMPLER_CONFIG,
|
||||
|
@ -441,6 +453,9 @@ export async function sampleForTransform(
|
|||
dataSource: 'sample',
|
||||
timestampSpec,
|
||||
dimensionsSpec: {},
|
||||
granularitySpec: {
|
||||
rollup: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
samplerConfig: BASE_SAMPLER_CONFIG,
|
||||
|
@ -474,6 +489,9 @@ export async function sampleForTransform(
|
|||
transformSpec: {
|
||||
transforms,
|
||||
},
|
||||
granularitySpec: {
|
||||
rollup: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
samplerConfig: BASE_SAMPLER_CONFIG,
|
||||
|
@ -502,6 +520,9 @@ export async function sampleForFilter(
|
|||
dataSource: 'sample',
|
||||
timestampSpec,
|
||||
dimensionsSpec: {},
|
||||
granularitySpec: {
|
||||
rollup: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
samplerConfig: BASE_SAMPLER_CONFIG,
|
||||
|
@ -536,6 +557,9 @@ export async function sampleForFilter(
|
|||
transforms,
|
||||
filter,
|
||||
},
|
||||
granularitySpec: {
|
||||
rollup: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
samplerConfig: BASE_SAMPLER_CONFIG,
|
||||
|
@ -556,6 +580,7 @@ export async function sampleForSchema(
|
|||
const metricsSpec: MetricSpec[] = deepGet(spec, 'spec.dataSchema.metricsSpec') || [];
|
||||
const queryGranularity: string =
|
||||
deepGet(spec, 'spec.dataSchema.granularitySpec.queryGranularity') || 'NONE';
|
||||
const rollup = deepGet(spec, 'spec.dataSchema.granularitySpec.rollup') ?? true;
|
||||
|
||||
const sampleSpec: SampleSpec = {
|
||||
type: samplerType,
|
||||
|
@ -567,6 +592,7 @@ export async function sampleForSchema(
|
|||
transformSpec,
|
||||
granularitySpec: {
|
||||
queryGranularity,
|
||||
rollup,
|
||||
},
|
||||
dimensionsSpec,
|
||||
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,
|
||||
Cell: ({ value }) => {
|
||||
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,
|
||||
Cell: ({ value }) => {
|
||||
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
|
||||
columnMetadataLoading={true}
|
||||
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]}
|
||||
onQueryChange={[Function]}
|
||||
/>
|
||||
|
@ -86,6 +159,79 @@ exports[`QueryView matches snapshot with query 1`] = `
|
|||
<ColumnTree
|
||||
columnMetadataLoading={true}
|
||||
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]}
|
||||
onQueryChange={[Function]}
|
||||
/>
|
||||
|
|
|
@ -127,6 +127,7 @@ exports[`ColumnTree matches snapshot 1`] = `
|
|||
</Blueprint3.Popover2>,
|
||||
},
|
||||
],
|
||||
"className": undefined,
|
||||
"icon": "th",
|
||||
"id": "wikipedia",
|
||||
"isExpanded": true,
|
||||
|
|
|
@ -42,17 +42,12 @@ const BETWEEN: SqlExpression = SqlExpression.parse(`(? <= ? AND ? < ?)`);
|
|||
// ------------------------------------
|
||||
|
||||
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 {
|
||||
const ref = SqlRef.column(columnName);
|
||||
return BETWEEN.fillPlaceholders([
|
||||
SqlLiteral.create(start),
|
||||
ref,
|
||||
ref,
|
||||
SqlLiteral.create(end),
|
||||
]) as SqlExpression;
|
||||
return BETWEEN.fillPlaceholders([SqlLiteral.create(start), ref, ref, SqlLiteral.create(end)])!;
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
|
|
|
@ -16,8 +16,18 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import '../../../variables';
|
||||
@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 {
|
||||
.bp3-dark & {
|
||||
background: $dark-gray3;
|
||||
|
@ -43,6 +53,10 @@
|
|||
.bp3-tree-node-content-1 {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
animation: druid-glow 1s infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-popover2-target {
|
||||
|
|
|
@ -39,9 +39,12 @@ import { NumberMenuItems, StringMenuItems, TimeMenuItems } from './column-tree-m
|
|||
|
||||
import './column-tree.scss';
|
||||
|
||||
const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
|
||||
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 {
|
||||
for (const columnName of columnNames) {
|
||||
if (columnName === 'count' || columnName === '__count') {
|
||||
|
@ -69,11 +72,20 @@ interface HandleColumnClickOptions {
|
|||
columnName: string;
|
||||
columnType: string;
|
||||
parsedQuery: SqlQuery | undefined;
|
||||
defaultWhere: SqlExpression | undefined;
|
||||
onQueryChange: (query: SqlQuery, run: boolean) => 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 where: SqlExpression | undefined;
|
||||
|
@ -84,7 +96,7 @@ function handleColumnShow(options: HandleColumnClickOptions): void {
|
|||
aggregates = parsedQuery.getAggregateSelectExpressions();
|
||||
} else if (columnSchema === 'druid') {
|
||||
from = SqlTableRef.create(columnTable);
|
||||
where = LAST_DAY;
|
||||
where = defaultWhere;
|
||||
} else {
|
||||
from = SqlTableRef.create(columnTable, columnSchema);
|
||||
}
|
||||
|
@ -118,9 +130,11 @@ export interface ColumnTreeProps {
|
|||
columnMetadataLoading: boolean;
|
||||
columnMetadata?: readonly ColumnMetadata[];
|
||||
getParsedQuery: () => SqlQuery | undefined;
|
||||
defaultWhere?: SqlExpression;
|
||||
onQueryChange: (query: SqlQuery, run?: boolean) => void;
|
||||
defaultSchema?: string;
|
||||
defaultTable?: string;
|
||||
highlightTable?: string;
|
||||
}
|
||||
|
||||
export interface ColumnTreeState {
|
||||
|
@ -154,7 +168,14 @@ export function getJoinColumns(parsedQuery: SqlQuery, _table: string) {
|
|||
|
||||
export class ColumnTree extends React.PureComponent<ColumnTreeProps, 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) {
|
||||
const columnTree = groupBy(
|
||||
|
@ -169,6 +190,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
(metadata, tableName): TreeNodeInfo => ({
|
||||
id: tableName,
|
||||
icon: IconNames.TH,
|
||||
className: tableName === highlightTable ? 'highlight' : undefined,
|
||||
label: (
|
||||
<Popover2
|
||||
position={Position.RIGHT}
|
||||
|
@ -195,7 +217,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
if (parsedQuery && parsedQuery.getFirstTableName() === tableName) {
|
||||
return parsedQuery.getWhereExpression();
|
||||
} else if (schemaName === 'druid') {
|
||||
return defaultToAllTime ? undefined : LAST_DAY;
|
||||
return defaultToAllTime ? undefined : defaultWhere;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
@ -210,7 +232,10 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
onQueryChange(
|
||||
getQueryOnTable()
|
||||
.changeSelectExpressions(
|
||||
metadata.map(child => SqlRef.column(child.COLUMN_NAME)),
|
||||
metadata
|
||||
.map(child => child.COLUMN_NAME)
|
||||
.sort(caseInsensitiveCompare)
|
||||
.map(columnName => SqlRef.column(columnName)),
|
||||
)
|
||||
.changeWhereExpression(getWhere()),
|
||||
true,
|
||||
|
@ -376,6 +401,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
columnName: columnData.COLUMN_NAME,
|
||||
columnType: columnData.DATA_TYPE,
|
||||
parsedQuery,
|
||||
defaultWhere,
|
||||
onQueryChange: onQueryChange,
|
||||
});
|
||||
}}
|
||||
|
@ -429,9 +455,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
),
|
||||
}),
|
||||
)
|
||||
.sort((a, b) =>
|
||||
String(a.id).toLowerCase().localeCompare(String(b.id).toLowerCase()),
|
||||
),
|
||||
.sort((a, b) => caseInsensitiveCompare(a.id, b.id)),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
@import '../../../blueprint-overrides/common/colors';
|
||||
|
||||
.query-input {
|
||||
position: relative;
|
||||
|
||||
.ace-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
|
|
@ -31,13 +31,14 @@ import {
|
|||
import React, { useEffect, useState } from 'react';
|
||||
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 {
|
||||
changePage,
|
||||
copyAndAlert,
|
||||
deepSet,
|
||||
filterMap,
|
||||
oneOf,
|
||||
formatNumber,
|
||||
getNumericColumnBraces,
|
||||
Pagination,
|
||||
prettyPrintSql,
|
||||
SMALL_TABLE_PAGE_SIZE,
|
||||
SMALL_TABLE_PAGE_SIZE_OPTIONS,
|
||||
|
@ -53,38 +54,6 @@ function isComparable(x: unknown): boolean {
|
|||
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 {
|
||||
queryResult: QueryResult;
|
||||
onQueryChange: (query: SqlQuery, run?: boolean) => void;
|
||||
|
@ -123,8 +92,8 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
|
|||
const orderByExpression = parsedQuery.isValidSelectIndex(headerIndex)
|
||||
? SqlLiteral.index(headerIndex)
|
||||
: SqlRef.column(header);
|
||||
const descOrderBy = orderByExpression.toOrderByPart('DESC');
|
||||
const ascOrderBy = orderByExpression.toOrderByPart('ASC');
|
||||
const descOrderBy = orderByExpression.toOrderByExpression('DESC');
|
||||
const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
|
||||
const orderBy = parsedQuery.getOrderByForSelectIndex(headerIndex);
|
||||
|
||||
const basicActions: BasicAction[] = [];
|
||||
|
@ -205,11 +174,11 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
|
|||
},
|
||||
});
|
||||
|
||||
return basicActionsToMenu(basicActions);
|
||||
return basicActionsToMenu(basicActions)!;
|
||||
} else {
|
||||
const orderByExpression = SqlRef.column(header);
|
||||
const descOrderBy = orderByExpression.toOrderByPart('DESC');
|
||||
const ascOrderBy = orderByExpression.toOrderByPart('ASC');
|
||||
const descOrderBy = orderByExpression.toOrderByExpression('DESC');
|
||||
const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
|
||||
const descOrderByPretty = prettyPrintSql(descOrderBy);
|
||||
const ascOrderByPretty = prettyPrintSql(descOrderBy);
|
||||
return (
|
||||
|
@ -409,7 +378,10 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
|
|||
? () => <ColumnRenameInput initialName={h} onDone={renameColumnTo} />
|
||||
: () => {
|
||||
return (
|
||||
<Popover2 className="clickable-cell" content={getHeaderMenu(h, i)}>
|
||||
<Popover2
|
||||
className="clickable-cell"
|
||||
content={<Deferred content={() => getHeaderMenu(h, i)} />}
|
||||
>
|
||||
<div>
|
||||
{h}
|
||||
{hasFilterOnHeader(h, i) && (
|
||||
|
@ -421,16 +393,17 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
|
|||
},
|
||||
headerClassName: getHeaderClassName(h, i),
|
||||
accessor: String(i),
|
||||
Cell: function QueryOutputTableCell(row) {
|
||||
Cell(row) {
|
||||
const value = row.value;
|
||||
return (
|
||||
<div>
|
||||
<Popover2 content={getCellMenu(h, i, value)}>
|
||||
<Popover2 content={<Deferred content={() => getCellMenu(h, i, value)} />}>
|
||||
{numericColumnBraces[i] ? (
|
||||
<BracedText
|
||||
text={String(value)}
|
||||
text={formatNumber(value)}
|
||||
braces={numericColumnBraces[i]}
|
||||
padFractionalPart
|
||||
unselectableThousandsSeparator
|
||||
/>
|
||||
) : (
|
||||
<TableCell value={value} unlimited />
|
||||
|
|
|
@ -20,12 +20,19 @@ import { IconName } from '@blueprintjs/core';
|
|||
import { IconNames } from '@blueprintjs/icons';
|
||||
|
||||
export function dataTypeToIcon(dataType: string): IconName {
|
||||
switch (dataType) {
|
||||
const typeUpper = dataType.toUpperCase();
|
||||
if (typeUpper.startsWith('COMPLEX')) {
|
||||
return IconNames.ASTERISK;
|
||||
}
|
||||
|
||||
switch (typeUpper) {
|
||||
case 'TIMESTAMP':
|
||||
return IconNames.TIME;
|
||||
case 'VARCHAR':
|
||||
case 'STRING':
|
||||
return IconNames.FONT;
|
||||
case 'BIGINT':
|
||||
case 'LONG':
|
||||
case 'FLOAT':
|
||||
case 'DOUBLE':
|
||||
return IconNames.NUMERICAL;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import { Code, Intent, Switch } from '@blueprintjs/core';
|
||||
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||
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 * as JSONBig from 'json-bigint-native';
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
@ -67,6 +67,8 @@ import { RunButton } from './run-button/run-button';
|
|||
|
||||
import './query-view.scss';
|
||||
|
||||
const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
|
||||
|
||||
const parser = memoizeOne((sql: string): SqlQuery | undefined => {
|
||||
try {
|
||||
return SqlQuery.parse(sql);
|
||||
|
@ -193,9 +195,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
},
|
||||
});
|
||||
|
||||
const queryRunner = new QueryRunner((payload, isSql, cancelToken) => {
|
||||
return Api.instance.post(`/druid/v2${isSql ? '/sql' : ''}`, payload, { cancelToken });
|
||||
});
|
||||
const queryRunner = new QueryRunner();
|
||||
|
||||
this.queryManager = new QueryManager({
|
||||
processQuery: async (
|
||||
|
@ -627,6 +627,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
getParsedQuery={this.getParsedQuery}
|
||||
columnMetadataLoading={columnMetadataState.loading}
|
||||
columnMetadata={columnMetadataState.data}
|
||||
defaultWhere={LAST_DAY}
|
||||
onQueryChange={this.handleQueryChange}
|
||||
defaultSchema={defaultSchema ? defaultSchema : 'druid'}
|
||||
defaultTable={defaultTable}
|
||||
|
|
|
@ -369,7 +369,7 @@ exports[`segments-view matches snapshot 1`] = `
|
|||
rowsText="rows"
|
||||
showPageJump={false}
|
||||
showPageSizeOptions={true}
|
||||
showPagination={false}
|
||||
showPagination={true}
|
||||
showPaginationBottom={true}
|
||||
showPaginationTop={false}
|
||||
sortable={true}
|
||||
|
|
|
@ -173,8 +173,8 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
|||
const columns = compact([
|
||||
visibleColumns.shown('Segment ID') && `"segment_id"`,
|
||||
visibleColumns.shown('Datasource') && `"datasource"`,
|
||||
visibleColumns.shown('Start') && `"start"`,
|
||||
visibleColumns.shown('End') && `"end"`,
|
||||
`"start"`,
|
||||
`"end"`,
|
||||
visibleColumns.shown('Version') && `"version"`,
|
||||
visibleColumns.shown('Time span') &&
|
||||
`CASE
|
||||
|
@ -191,6 +191,7 @@ END AS "time_span"`,
|
|||
WHEN "shard_spec" LIKE '%"type":"numbered"%' THEN 'dynamic'
|
||||
WHEN "shard_spec" LIKE '%"type":"hashed"%' THEN 'hashed'
|
||||
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":"linear"%' THEN 'linear'
|
||||
WHEN "shard_spec" LIKE '%"type":"numbered_overwrite"%' THEN 'numbered_overwrite'
|
||||
|
@ -206,10 +207,6 @@ END AS "partitioning"`,
|
|||
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)`;
|
||||
}
|
||||
|
||||
|
@ -516,7 +513,7 @@ END AS "partitioning"`,
|
|||
pivotBy={groupByInterval ? ['interval'] : []}
|
||||
defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
|
||||
pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
|
||||
showPagination={segments.length > STANDARD_TABLE_PAGE_SIZE}
|
||||
showPagination
|
||||
columns={[
|
||||
{
|
||||
Header: 'Segment ID',
|
||||
|
@ -625,6 +622,7 @@ END AS "partitioning"`,
|
|||
<BracedText
|
||||
text={row.original.is_available ? formatInteger(row.value) : '(unknown)'}
|
||||
braces={numRowsValues}
|
||||
unselectableThousandsSeparator
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -183,7 +183,7 @@ exports[`ServicesView renders data 1`] = `
|
|||
"filterable": false,
|
||||
"id": "usage",
|
||||
"show": true,
|
||||
"width": 100,
|
||||
"width": 140,
|
||||
},
|
||||
Object {
|
||||
"Aggregated": [Function],
|
||||
|
|
|
@ -444,7 +444,7 @@ ORDER BY "rank" DESC, "service" DESC`;
|
|||
Header: 'Usage',
|
||||
show: visibleColumns.shown('Usage'),
|
||||
id: 'usage',
|
||||
width: 100,
|
||||
width: 140,
|
||||
filterable: false,
|
||||
accessor: row => {
|
||||
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 capacity = deepGet(row, 'original.worker.capacity');
|
||||
if (typeof capacity === 'number') {
|
||||
return `${currCapacityUsed} / ${capacity} (slots)`;
|
||||
return `Slots used: ${currCapacityUsed} of ${capacity}`;
|
||||
} else {
|
||||
return '- / -';
|
||||
return 'Slots used: -';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue