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

* don't copy commas

* use numeric type information

* add VALUES keyword

* propogate rollup config into spec

* fix

* cleanup

* understand range partitioning

* update snapshots

* better comp apis

* fix segment pages

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

View File

@ -5105,7 +5105,7 @@ license_category: binary
module: web-console
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
---

View File

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

View File

@ -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"
}

View File

@ -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",

View File

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

View File

@ -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>

View File

@ -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) {

View File

@ -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';

View File

@ -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

View File

@ -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}
/>
);
}

View File

@ -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';

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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} />

View File

@ -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

View File

@ -1230,16 +1230,7 @@ export function fillDataSourceNameIfNeeded(spec: Partial<IngestionSpec>): Partia
return deepSetIfUnset(spec, 'spec.dataSchema.dataSource', possibleName);
}
export function guessDataSourceName(spec: Partial<IngestionSpec>): string | undefined {
const ioConfig = deepGet(spec, 'spec.ioConfig');
if (!ioConfig) return;
switch (ioConfig.type) {
case 'index':
case 'index_parallel': {
const inputSource = ioConfig.inputSource;
if (!inputSource) return;
export function guessDataSourceNameFromInputSource(inputSource: InputSource): string | undefined {
switch (inputSource.type) {
case 'local':
if (inputSource.filter && filterIsFilename(inputSource.filter)) {
@ -1260,19 +1251,30 @@ export function guessDataSourceName(spec: Partial<IngestionSpec>): string | unde
}
case 'http':
return Array.isArray(inputSource.uris)
? filenameFromPath(inputSource.uris[0])
: undefined;
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;
switch (ioConfig.type) {
case 'index':
case 'index_parallel': {
const inputSource = ioConfig.inputSource;
if (!inputSource) return;
return guessDataSourceNameFromInputSource(inputSource);
}
case 'kafka':
return ioConfig.topic;
@ -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'),
),
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'),
),
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.

View File

@ -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,

View File

@ -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';

View File

@ -167,6 +167,14 @@ export function typeIs<T extends { type?: S }, S = string>(...options: S[]): (x:
};
}
export function without<T>(xs: readonly T[], x: T | undefined): T[] {
return xs.filter(i => i !== x);
}
export function change<T>(xs: readonly T[], from: T, to: T): T[] {
return xs.map(x => (x === from ? to : x));
}
// ----------------------------
export function countBy<T>(
@ -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}`;

View File

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

View File

@ -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,

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { QueryResult } from 'druid-query-toolkit';
import { filterMap, formatNumber, oneOf } from './general';
import { deepSet } from './object-change';
export interface Pagination {
page: number;
pageSize: number;
}
export function changePage(pagination: Pagination, page: number): Pagination {
return deepSet(pagination, 'page', page);
}
export function getNumericColumnBraces(
queryResult: QueryResult,
pagination?: Pagination,
): Record<number, string[]> {
let rows = queryResult.rows;
if (pagination) {
const index = pagination.page * pagination.pageSize;
rows = rows.slice(index, index + pagination.pageSize);
}
const numericColumnBraces: Record<number, string[]> = {};
if (rows.length) {
queryResult.header.forEach((column, i) => {
if (!oneOf(column.nativeType, 'LONG', 'FLOAT', 'DOUBLE')) return;
const brace = filterMap(rows, row =>
oneOf(typeof row[i], 'number', 'bigint') ? formatNumber(row[i]) : undefined,
);
if (rows.length === brace.length) {
numericColumnBraces[i] = brace;
}
});
}
return numericColumnBraces;
}

View File

@ -1247,7 +1247,13 @@ ORDER BY 1`;
width: 100,
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
/>
);
},
},
{

View File

@ -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]}
/>

View File

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

View File

@ -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)])!;
}
// ------------------------------------

View File

@ -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 {

View File

@ -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)),
}),
),
}),

View File

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

View File

@ -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 />

View File

@ -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;

View File

@ -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}

View File

@ -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}

View File

@ -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
/>
),
},

View File

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

View File

@ -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: -';
}
}