mirror of https://github.com/apache/druid.git
[backport] Backport 24.0.1 web console issue fixes (#13146)
* fix number of expected functions (#13050) * default to no compare (#13041) * quote columns, datasources in auto complete if needed (#13060) * Web console: better detection for arrays containing objects (#13077) * better detection for arrays containing objects * include boolean also * link to error docs (#13094) * Web console: correctly escape path based flatten specs (#13105) * fix path generation * do escape * fix replace * fix replace for good * append to exisitng callout (#13130) * better spec conversion with issues (#13136) * bump version to 24.0.1
This commit is contained in:
parent
9de988bec6
commit
cffa3bd263
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "web-console",
|
"name": "web-console",
|
||||||
"version": "24.0.0",
|
"version": "24.0.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "web-console",
|
"name": "web-console",
|
||||||
"version": "24.0.0",
|
"version": "24.0.1",
|
||||||
"description": "A web console for Apache Druid",
|
"description": "A web console for Apache Druid",
|
||||||
"author": "Apache Druid Developers <dev@druid.apache.org>",
|
"author": "Apache Druid Developers <dev@druid.apache.org>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|
|
@ -23,7 +23,7 @@ const snarkdown = require('snarkdown');
|
||||||
|
|
||||||
const writefile = 'lib/sql-docs.js';
|
const writefile = 'lib/sql-docs.js';
|
||||||
|
|
||||||
const MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS = 158;
|
const MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS = 162;
|
||||||
const MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES = 14;
|
const MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES = 14;
|
||||||
|
|
||||||
function hasHtmlTags(str) {
|
function hasHtmlTags(str) {
|
||||||
|
@ -90,15 +90,15 @@ const readDoc = async () => {
|
||||||
|
|
||||||
// Make sure there are enough functions found
|
// Make sure there are enough functions found
|
||||||
const numFunction = Object.keys(functionDocs).length;
|
const numFunction = Object.keys(functionDocs).length;
|
||||||
if (numFunction < MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS) {
|
if (!(MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS <= numFunction)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Did not find enough function entries did the structure of '${readfile}' change? (found ${numFunction} but expected at least ${MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS})`,
|
`Did not find enough function entries did the structure of '${readfile}' change? (found ${numFunction} but expected at least ${MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there are at least 10 data types for sanity
|
// Make sure there are at least 10 data types for sanity
|
||||||
const numDataTypes = dataTypeDocs.length;
|
const numDataTypes = Object.keys(dataTypeDocs).length;
|
||||||
if (numDataTypes < MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES) {
|
if (!(MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES <= numDataTypes)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Did not find enough data type entries did the structure of '${readfile}' change? (found ${numDataTypes} but expected at least ${MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES})`,
|
`Did not find enough data type entries did the structure of '${readfile}' change? (found ${numDataTypes} but expected at least ${MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES})`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,6 +63,10 @@ ace.define(
|
||||||
|
|
||||||
this.$rules = {
|
this.$rules = {
|
||||||
start: [
|
start: [
|
||||||
|
{
|
||||||
|
token: 'comment.issue',
|
||||||
|
regex: '--:ISSUE:.*$',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
token: 'comment',
|
token: 'comment',
|
||||||
regex: '--.*$',
|
regex: '--.*$',
|
||||||
|
@ -73,17 +77,13 @@ ace.define(
|
||||||
end: '\\*/',
|
end: '\\*/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: 'string', // " string
|
token: 'variable.column', // " quoted reference
|
||||||
regex: '".*?"',
|
regex: '".*?"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
token: 'string', // ' string
|
token: 'string', // ' string literal
|
||||||
regex: "'.*?'",
|
regex: "'.*?'",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
token: 'string', // ` string (apache drill)
|
|
||||||
regex: '`.*?`',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
token: 'constant.numeric', // float
|
token: 'constant.numeric', // float
|
||||||
regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b',
|
regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b',
|
||||||
|
|
|
@ -25,6 +25,18 @@
|
||||||
.ace-solarized-dark {
|
.ace-solarized-dark {
|
||||||
background-color: rgba($dark-gray1, 0.5);
|
background-color: rgba($dark-gray1, 0.5);
|
||||||
|
|
||||||
|
// START: Custom code styles
|
||||||
|
.ace_variable.ace_column {
|
||||||
|
color: #2ceefb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace_comment.ace_issue {
|
||||||
|
color: #cb3116;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: wavy;
|
||||||
|
}
|
||||||
|
// END: Custom code styles
|
||||||
|
|
||||||
&.no-background {
|
&.no-background {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,7 +326,7 @@ exports[`HeaderBar matches snapshot 1`] = `
|
||||||
<Blueprint4.MenuItem
|
<Blueprint4.MenuItem
|
||||||
active={false}
|
active={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
href="https://druid.apache.org/docs/24.0.0"
|
href="https://druid.apache.org/docs/24.0.1"
|
||||||
icon="th"
|
icon="th"
|
||||||
multiline={false}
|
multiline={false}
|
||||||
popoverProps={Object {}}
|
popoverProps={Object {}}
|
||||||
|
|
|
@ -49,6 +49,14 @@ exports[`TableCell matches snapshot array long 1`] = `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`TableCell matches snapshot array mixed 1`] = `
|
||||||
|
<div
|
||||||
|
class="table-cell plain"
|
||||||
|
>
|
||||||
|
["a",{"v":"b"},"c"]
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`TableCell matches snapshot array short 1`] = `
|
exports[`TableCell matches snapshot array short 1`] = `
|
||||||
<div
|
<div
|
||||||
class="table-cell plain"
|
class="table-cell plain"
|
||||||
|
|
|
@ -64,6 +64,13 @@ describe('TableCell', () => {
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches snapshot array mixed', () => {
|
||||||
|
const tableCell = <TableCell value={['a', { v: 'b' }, 'c']} />;
|
||||||
|
|
||||||
|
const { container } = render(tableCell);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it('matches snapshot object', () => {
|
it('matches snapshot object', () => {
|
||||||
const tableCell = <TableCell value={{ hello: 'world' }} />;
|
const tableCell = <TableCell value={{ hello: 'world' }} />;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import * as JSONBig from 'json-bigint-native';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { ShowValueDialog } from '../../dialogs/show-value-dialog/show-value-dialog';
|
import { ShowValueDialog } from '../../dialogs/show-value-dialog/show-value-dialog';
|
||||||
|
import { isSimpleArray } from '../../utils';
|
||||||
import { ActionIcon } from '../action-icon/action-icon';
|
import { ActionIcon } from '../action-icon/action-icon';
|
||||||
|
|
||||||
import './table-cell.scss';
|
import './table-cell.scss';
|
||||||
|
@ -97,7 +98,7 @@ export const TableCell = React.memo(function TableCell(props: TableCellProps) {
|
||||||
{isNaN(dateValue) ? 'Unusable date' : value.toISOString()}
|
{isNaN(dateValue) ? 'Unusable date' : value.toISOString()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (Array.isArray(value)) {
|
} else if (isSimpleArray(value)) {
|
||||||
return renderTruncated(`[${value.join(', ')}]`);
|
return renderTruncated(`[${value.join(', ')}]`);
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === 'object') {
|
||||||
return renderTruncated(JSONBig.stringify(value));
|
return renderTruncated(JSONBig.stringify(value));
|
||||||
|
|
|
@ -34,14 +34,14 @@ export interface TableFilterableCellProps {
|
||||||
value: string;
|
value: string;
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
onFiltersChange(filters: Filter[]): void;
|
onFiltersChange(filters: Filter[]): void;
|
||||||
disableComparisons?: boolean;
|
enableComparisons?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableFilterableCell = React.memo(function TableFilterableCell(
|
export const TableFilterableCell = React.memo(function TableFilterableCell(
|
||||||
props: TableFilterableCellProps,
|
props: TableFilterableCellProps,
|
||||||
) {
|
) {
|
||||||
const { field, value, children, filters, disableComparisons, onFiltersChange } = props;
|
const { field, value, children, filters, enableComparisons, onFiltersChange } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover2
|
<Popover2
|
||||||
|
@ -51,7 +51,7 @@ export const TableFilterableCell = React.memo(function TableFilterableCell(
|
||||||
content={() => (
|
content={() => (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuDivider title="Filter" />
|
<MenuDivider title="Filter" />
|
||||||
{(disableComparisons ? FILTER_MODES_NO_COMPARISONS : FILTER_MODES).map((mode, i) => (
|
{(enableComparisons ? FILTER_MODES : FILTER_MODES_NO_COMPARISONS).map((mode, i) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={i}
|
key={i}
|
||||||
icon={filterModeToIcon(mode)}
|
icon={filterModeToIcon(mode)}
|
||||||
|
|
|
@ -12,7 +12,7 @@ exports[`CoordinatorDynamicConfigDialog matches snapshot 1`] = `
|
||||||
Edit the coordinator dynamic configuration on the fly. For more information please refer to the
|
Edit the coordinator dynamic configuration on the fly. For more information please refer to the
|
||||||
|
|
||||||
<Memo(ExternalLink)
|
<Memo(ExternalLink)
|
||||||
href="https://druid.apache.org/docs/24.0.0/configuration/index.html#dynamic-configuration"
|
href="https://druid.apache.org/docs/24.0.1/configuration/index.html#dynamic-configuration"
|
||||||
>
|
>
|
||||||
documentation
|
documentation
|
||||||
</Memo(ExternalLink)>
|
</Memo(ExternalLink)>
|
||||||
|
|
|
@ -11,7 +11,7 @@ exports[`OverlordDynamicConfigDialog matches snapshot 1`] = `
|
||||||
Edit the overlord dynamic configuration on the fly. For more information please refer to the
|
Edit the overlord dynamic configuration on the fly. For more information please refer to the
|
||||||
|
|
||||||
<Memo(ExternalLink)
|
<Memo(ExternalLink)
|
||||||
href="https://druid.apache.org/docs/24.0.0/configuration/index.html#overlord-dynamic-configuration"
|
href="https://druid.apache.org/docs/24.0.1/configuration/index.html#overlord-dynamic-configuration"
|
||||||
>
|
>
|
||||||
documentation
|
documentation
|
||||||
</Memo(ExternalLink)>
|
</Memo(ExternalLink)>
|
||||||
|
|
|
@ -63,7 +63,7 @@ exports[`RetentionDialog matches snapshot 1`] = `
|
||||||
Druid uses rules to determine what data should be retained in the cluster. The rules are evaluated in order from top to bottom. For more information please refer to the
|
Druid uses rules to determine what data should be retained in the cluster. The rules are evaluated in order from top to bottom. For more information please refer to the
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://druid.apache.org/docs/24.0.0/operations/rule-configuration.html"
|
href="https://druid.apache.org/docs/24.0.1/operations/rule-configuration.html"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe('flatten-spec', () => {
|
||||||
describe('computeFlattenExprsForData', () => {
|
describe('computeFlattenExprsForData', () => {
|
||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
context: { host: 'cla', topic: 'moon', bonus: { foo: 'bar' } },
|
context: { host: 'cla', topic: 'moon', bonus: { 'fo.o': 'bar' } },
|
||||||
tags: ['a', 'b', 'c'],
|
tags: ['a', 'b', 'c'],
|
||||||
messages: [
|
messages: [
|
||||||
{ metric: 'request/time', value: 122 },
|
{ metric: 'request/time', value: 122 },
|
||||||
|
@ -32,7 +32,7 @@ describe('flatten-spec', () => {
|
||||||
value: 5,
|
value: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
context: { host: 'piv', popic: 'sun' },
|
context: { 'host': 'piv', '1pic': 'sun' },
|
||||||
tags: ['a', 'd'],
|
tags: ['a', 'd'],
|
||||||
messages: [
|
messages: [
|
||||||
{ metric: 'request/time', value: 44 },
|
{ metric: 'request/time', value: 44 },
|
||||||
|
@ -41,7 +41,7 @@ describe('flatten-spec', () => {
|
||||||
value: 4,
|
value: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
context: { host: 'imp', dopik: 'fun' },
|
context: { 'host': 'imp', "d\\o\npi'c'": 'fun' },
|
||||||
tags: ['x', 'y'],
|
tags: ['x', 'y'],
|
||||||
messages: [
|
messages: [
|
||||||
{ metric: 'request/time', value: 4 },
|
{ metric: 'request/time', value: 4 },
|
||||||
|
@ -53,22 +53,12 @@ describe('flatten-spec', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
it('works for path, ignore-arrays', () => {
|
it('works for path, ignore-arrays', () => {
|
||||||
expect(computeFlattenExprsForData(data, 'path', 'ignore-arrays')).toEqual([
|
expect(computeFlattenExprsForData(data, 'ignore-arrays')).toEqual([
|
||||||
'$.context.bonus.foo',
|
"$.context.bonus['fo.o']",
|
||||||
'$.context.dopik',
|
|
||||||
'$.context.host',
|
'$.context.host',
|
||||||
'$.context.popic',
|
|
||||||
'$.context.topic',
|
'$.context.topic',
|
||||||
]);
|
"$.context['1pic']",
|
||||||
});
|
"$.context['d\\\\o\npi\\'c\\'']",
|
||||||
|
|
||||||
it('works for jq, ignore-arrays', () => {
|
|
||||||
expect(computeFlattenExprsForData(data, 'jq', 'ignore-arrays')).toEqual([
|
|
||||||
'.context.bonus.foo',
|
|
||||||
'.context.dopik',
|
|
||||||
'.context.host',
|
|
||||||
'.context.popic',
|
|
||||||
'.context.topic',
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,18 +61,22 @@ export const FLATTEN_FIELD_FIELDS: Field<FlattenField>[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type ExprType = 'path' | 'jq';
|
|
||||||
export type ArrayHandling = 'ignore-arrays' | 'include-arrays';
|
export type ArrayHandling = 'ignore-arrays' | 'include-arrays';
|
||||||
|
|
||||||
|
function escapePathKey(pathKey: string): string {
|
||||||
|
return /^[a-z]\w*$/i.test(pathKey)
|
||||||
|
? `.${pathKey}`
|
||||||
|
: `['${pathKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}']`;
|
||||||
|
}
|
||||||
|
|
||||||
export function computeFlattenPathsForData(
|
export function computeFlattenPathsForData(
|
||||||
data: Record<string, any>[],
|
data: Record<string, any>[],
|
||||||
exprType: ExprType,
|
|
||||||
arrayHandling: ArrayHandling,
|
arrayHandling: ArrayHandling,
|
||||||
): FlattenField[] {
|
): FlattenField[] {
|
||||||
return computeFlattenExprsForData(data, exprType, arrayHandling).map(expr => {
|
return computeFlattenExprsForData(data, arrayHandling).map(expr => {
|
||||||
return {
|
return {
|
||||||
name: expr.replace(/^\$?\./, ''),
|
name: expr.replace(/^\$\./, '').replace(/['\]]/g, '').replace(/\[/g, '.'),
|
||||||
type: exprType,
|
type: 'path',
|
||||||
expr,
|
expr,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -80,7 +84,6 @@ export function computeFlattenPathsForData(
|
||||||
|
|
||||||
export function computeFlattenExprsForData(
|
export function computeFlattenExprsForData(
|
||||||
data: Record<string, any>[],
|
data: Record<string, any>[],
|
||||||
exprType: ExprType,
|
|
||||||
arrayHandling: ArrayHandling,
|
arrayHandling: ArrayHandling,
|
||||||
includeTopLevel = false,
|
includeTopLevel = false,
|
||||||
): string[] {
|
): string[] {
|
||||||
|
@ -91,12 +94,7 @@ export function computeFlattenExprsForData(
|
||||||
for (const datumKey of datumKeys) {
|
for (const datumKey of datumKeys) {
|
||||||
const datumValue = datum[datumKey];
|
const datumValue = datum[datumKey];
|
||||||
if (includeTopLevel || isNested(datumValue)) {
|
if (includeTopLevel || isNested(datumValue)) {
|
||||||
addPath(
|
addPath(seenPaths, `$${escapePathKey(datumKey)}`, datumValue, arrayHandling);
|
||||||
seenPaths,
|
|
||||||
exprType === 'path' ? `$.${datumKey}` : `.${datumKey}`,
|
|
||||||
datumValue,
|
|
||||||
arrayHandling,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,7 +112,7 @@ function addPath(
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
const valueKeys = Object.keys(value);
|
const valueKeys = Object.keys(value);
|
||||||
for (const valueKey of valueKeys) {
|
for (const valueKey of valueKeys) {
|
||||||
addPath(paths, `${path}.${valueKey}`, value[valueKey], arrayHandling);
|
addPath(paths, `${path}${escapePathKey(valueKey)}`, value[valueKey], arrayHandling);
|
||||||
}
|
}
|
||||||
} else if (arrayHandling === 'include-arrays') {
|
} else if (arrayHandling === 'include-arrays') {
|
||||||
for (let i = 0; i < value.length; i++) {
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
|
|
@ -725,6 +725,15 @@ describe('spec utils', () => {
|
||||||
it('works for multi-value', () => {
|
it('works for multi-value', () => {
|
||||||
expect(guessColumnTypeFromInput(['a', ['b'], 'c'], false)).toEqual('string');
|
expect(guessColumnTypeFromInput(['a', ['b'], 'c'], false)).toEqual('string');
|
||||||
expect(guessColumnTypeFromInput([1, [2], 3], false)).toEqual('string');
|
expect(guessColumnTypeFromInput([1, [2], 3], false)).toEqual('string');
|
||||||
|
expect(guessColumnTypeFromInput([true, [true, 7, false], false, 'x'], false)).toEqual(
|
||||||
|
'string',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works for complex arrays', () => {
|
||||||
|
expect(guessColumnTypeFromInput([{ type: 'Dogs' }, { type: 'JavaScript' }], false)).toEqual(
|
||||||
|
'COMPLEX<json>',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works for strange json', () => {
|
it('works for strange json', () => {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
EMPTY_ARRAY,
|
EMPTY_ARRAY,
|
||||||
EMPTY_OBJECT,
|
EMPTY_OBJECT,
|
||||||
filterMap,
|
filterMap,
|
||||||
|
isSimpleArray,
|
||||||
oneOf,
|
oneOf,
|
||||||
parseCsvLine,
|
parseCsvLine,
|
||||||
typeIs,
|
typeIs,
|
||||||
|
@ -2309,7 +2310,7 @@ export function guessIsArrayFromHeaderAndRows(
|
||||||
headerAndRows: SampleHeaderAndRows,
|
headerAndRows: SampleHeaderAndRows,
|
||||||
column: string,
|
column: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return headerAndRows.rows.some(r => Array.isArray(r.input?.[column]));
|
return headerAndRows.rows.some(r => isSimpleArray(r.input?.[column]));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function guessColumnTypeFromInput(
|
export function guessColumnTypeFromInput(
|
||||||
|
@ -2322,7 +2323,7 @@ export function guessColumnTypeFromInput(
|
||||||
if (!definedValues.length) return 'string';
|
if (!definedValues.length) return 'string';
|
||||||
|
|
||||||
// If we see any arrays in the input this is a multi-value dimension that must be a string
|
// If we see any arrays in the input this is a multi-value dimension that must be a string
|
||||||
if (definedValues.some(v => Array.isArray(v))) return 'string';
|
if (definedValues.some(v => isSimpleArray(v))) return 'string';
|
||||||
|
|
||||||
// If we see any JSON objects in the input assume COMPLEX<json>
|
// If we see any JSON objects in the input assume COMPLEX<json>
|
||||||
if (definedValues.some(v => v && typeof v === 'object')) return 'COMPLEX<json>';
|
if (definedValues.some(v => v && typeof v === 'object')) return 'COMPLEX<json>';
|
||||||
|
|
|
@ -423,6 +423,20 @@ describe('WorkbenchQuery', () => {
|
||||||
sqlPrefixLines: 0,
|
sqlPrefixLines: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('works with sql with ISSUE comment', () => {
|
||||||
|
const sql = sane`
|
||||||
|
SELECT *
|
||||||
|
--:ISSUE: There is something wrong with this query.
|
||||||
|
FROM wikipedia
|
||||||
|
`;
|
||||||
|
|
||||||
|
const workbenchQuery = WorkbenchQuery.blank().changeQueryString(sql);
|
||||||
|
|
||||||
|
expect(() => workbenchQuery.getApiQuery(makeQueryId)).toThrow(
|
||||||
|
`This query contains an ISSUE comment: There is something wrong with this query. (Please resolve the issue in the comment, delete the ISSUE comment and re-run the query.)`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#getIngestDatasource', () => {
|
describe('#getIngestDatasource', () => {
|
||||||
|
|
|
@ -576,10 +576,15 @@ export class WorkbenchQuery {
|
||||||
apiQuery.query = queryPrepend + apiQuery.query + queryAppend;
|
apiQuery.query = queryPrepend + apiQuery.query + queryAppend;
|
||||||
}
|
}
|
||||||
|
|
||||||
const m = /(--:context\s.+)(?:\n|$)/.exec(apiQuery.query);
|
const m = /--:ISSUE:(.+)(?:\n|$)/.exec(apiQuery.query);
|
||||||
if (m) {
|
if (m) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`This query contains a context comment '${m[1]}'. Context comments have been deprecated. Please rewrite the context comment as a context parameter. The context parameter editor is located in the "Engine" dropdown.`,
|
`This query contains an ISSUE comment: ${m[1]
|
||||||
|
.trim()
|
||||||
|
.replace(
|
||||||
|
/\.$/,
|
||||||
|
'',
|
||||||
|
)}. (Please resolve the issue in the comment, delete the ISSUE comment and re-run the query.)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -449,4 +449,262 @@ describe('spec conversion', () => {
|
||||||
finalizeAggregations: false,
|
finalizeAggregations: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('converts with issue when there is a __time transform', () => {
|
||||||
|
const converted = convertSpecToSql({
|
||||||
|
type: 'index_parallel',
|
||||||
|
spec: {
|
||||||
|
ioConfig: {
|
||||||
|
type: 'index_parallel',
|
||||||
|
inputSource: {
|
||||||
|
type: 'http',
|
||||||
|
uris: ['https://druid.apache.org/data/wikipedia.json.gz'],
|
||||||
|
},
|
||||||
|
inputFormat: {
|
||||||
|
type: 'json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataSchema: {
|
||||||
|
granularitySpec: {
|
||||||
|
segmentGranularity: 'hour',
|
||||||
|
queryGranularity: 'none',
|
||||||
|
rollup: false,
|
||||||
|
},
|
||||||
|
dataSource: 'wikipedia',
|
||||||
|
transformSpec: {
|
||||||
|
transforms: [{ name: '__time', expression: '_some_time_parse_expression_' }],
|
||||||
|
},
|
||||||
|
timestampSpec: {
|
||||||
|
column: 'timestamp',
|
||||||
|
format: 'auto',
|
||||||
|
},
|
||||||
|
dimensionsSpec: {
|
||||||
|
dimensions: [
|
||||||
|
'isRobot',
|
||||||
|
'channel',
|
||||||
|
'flags',
|
||||||
|
'isUnpatrolled',
|
||||||
|
'page',
|
||||||
|
'diffUrl',
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'added',
|
||||||
|
},
|
||||||
|
'comment',
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'commentLength',
|
||||||
|
},
|
||||||
|
'isNew',
|
||||||
|
'isMinor',
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'delta',
|
||||||
|
},
|
||||||
|
'isAnonymous',
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'deltaBucket',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'deleted',
|
||||||
|
},
|
||||||
|
'namespace',
|
||||||
|
'cityName',
|
||||||
|
'countryName',
|
||||||
|
'regionIsoCode',
|
||||||
|
'metroCode',
|
||||||
|
'countryIsoCode',
|
||||||
|
'regionName',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tuningConfig: {
|
||||||
|
type: 'index_parallel',
|
||||||
|
partitionsSpec: {
|
||||||
|
type: 'single_dim',
|
||||||
|
partitionDimension: 'isRobot',
|
||||||
|
targetRowsPerSegment: 150000,
|
||||||
|
},
|
||||||
|
forceGuaranteedRollup: true,
|
||||||
|
maxNumConcurrentSubTasks: 4,
|
||||||
|
maxParseExceptions: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(converted.queryString).toEqual(sane`
|
||||||
|
-- This SQL query was auto generated from an ingestion spec
|
||||||
|
REPLACE INTO wikipedia OVERWRITE ALL
|
||||||
|
WITH source AS (SELECT * FROM TABLE(
|
||||||
|
EXTERN(
|
||||||
|
'{"type":"http","uris":["https://druid.apache.org/data/wikipedia.json.gz"]}',
|
||||||
|
'{"type":"json"}',
|
||||||
|
'[{"name":"isRobot","type":"string"},{"name":"channel","type":"string"},{"name":"flags","type":"string"},{"name":"isUnpatrolled","type":"string"},{"name":"page","type":"string"},{"name":"diffUrl","type":"string"},{"name":"added","type":"long"},{"name":"comment","type":"string"},{"name":"commentLength","type":"long"},{"name":"isNew","type":"string"},{"name":"isMinor","type":"string"},{"name":"delta","type":"long"},{"name":"isAnonymous","type":"string"},{"name":"user","type":"string"},{"name":"deltaBucket","type":"long"},{"name":"deleted","type":"long"},{"name":"namespace","type":"string"},{"name":"cityName","type":"string"},{"name":"countryName","type":"string"},{"name":"regionIsoCode","type":"string"},{"name":"metroCode","type":"string"},{"name":"countryIsoCode","type":"string"},{"name":"regionName","type":"string"}]'
|
||||||
|
)
|
||||||
|
))
|
||||||
|
SELECT
|
||||||
|
--:ISSUE: The spec contained transforms that could not be automatically converted.
|
||||||
|
REWRITE_[_some_time_parse_expression_]_TO_SQL AS __time, --:ISSUE: Transform for __time could not be converted
|
||||||
|
"isRobot",
|
||||||
|
"channel",
|
||||||
|
"flags",
|
||||||
|
"isUnpatrolled",
|
||||||
|
"page",
|
||||||
|
"diffUrl",
|
||||||
|
"added",
|
||||||
|
"comment",
|
||||||
|
"commentLength",
|
||||||
|
"isNew",
|
||||||
|
"isMinor",
|
||||||
|
"delta",
|
||||||
|
"isAnonymous",
|
||||||
|
"user",
|
||||||
|
"deltaBucket",
|
||||||
|
"deleted",
|
||||||
|
"namespace",
|
||||||
|
"cityName",
|
||||||
|
"countryName",
|
||||||
|
"regionIsoCode",
|
||||||
|
"metroCode",
|
||||||
|
"countryIsoCode",
|
||||||
|
"regionName"
|
||||||
|
FROM source
|
||||||
|
PARTITIONED BY HOUR
|
||||||
|
CLUSTERED BY "isRobot"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts with issue when there is a dimension transform and strange filter', () => {
|
||||||
|
const converted = convertSpecToSql({
|
||||||
|
type: 'index_parallel',
|
||||||
|
spec: {
|
||||||
|
ioConfig: {
|
||||||
|
type: 'index_parallel',
|
||||||
|
inputSource: {
|
||||||
|
type: 'http',
|
||||||
|
uris: ['https://druid.apache.org/data/wikipedia.json.gz'],
|
||||||
|
},
|
||||||
|
inputFormat: {
|
||||||
|
type: 'json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataSchema: {
|
||||||
|
granularitySpec: {
|
||||||
|
segmentGranularity: 'hour',
|
||||||
|
queryGranularity: 'none',
|
||||||
|
rollup: false,
|
||||||
|
},
|
||||||
|
dataSource: 'wikipedia',
|
||||||
|
transformSpec: {
|
||||||
|
transforms: [{ name: 'comment', expression: '_some_expression_' }],
|
||||||
|
filter: {
|
||||||
|
type: 'strange',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timestampSpec: {
|
||||||
|
column: 'timestamp',
|
||||||
|
format: 'auto',
|
||||||
|
},
|
||||||
|
dimensionsSpec: {
|
||||||
|
dimensions: [
|
||||||
|
'isRobot',
|
||||||
|
'channel',
|
||||||
|
'flags',
|
||||||
|
'isUnpatrolled',
|
||||||
|
'page',
|
||||||
|
'diffUrl',
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'added',
|
||||||
|
},
|
||||||
|
'comment',
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'commentLength',
|
||||||
|
},
|
||||||
|
'isNew',
|
||||||
|
'isMinor',
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'delta',
|
||||||
|
},
|
||||||
|
'isAnonymous',
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'deltaBucket',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'long',
|
||||||
|
name: 'deleted',
|
||||||
|
},
|
||||||
|
'namespace',
|
||||||
|
'cityName',
|
||||||
|
'countryName',
|
||||||
|
'regionIsoCode',
|
||||||
|
'metroCode',
|
||||||
|
'countryIsoCode',
|
||||||
|
'regionName',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tuningConfig: {
|
||||||
|
type: 'index_parallel',
|
||||||
|
partitionsSpec: {
|
||||||
|
type: 'single_dim',
|
||||||
|
partitionDimension: 'isRobot',
|
||||||
|
targetRowsPerSegment: 150000,
|
||||||
|
},
|
||||||
|
forceGuaranteedRollup: true,
|
||||||
|
maxNumConcurrentSubTasks: 4,
|
||||||
|
maxParseExceptions: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(converted.queryString).toEqual(sane`
|
||||||
|
-- This SQL query was auto generated from an ingestion spec
|
||||||
|
REPLACE INTO wikipedia OVERWRITE ALL
|
||||||
|
WITH source AS (SELECT * FROM TABLE(
|
||||||
|
EXTERN(
|
||||||
|
'{"type":"http","uris":["https://druid.apache.org/data/wikipedia.json.gz"]}',
|
||||||
|
'{"type":"json"}',
|
||||||
|
'[{"name":"timestamp","type":"string"},{"name":"isRobot","type":"string"},{"name":"channel","type":"string"},{"name":"flags","type":"string"},{"name":"isUnpatrolled","type":"string"},{"name":"page","type":"string"},{"name":"diffUrl","type":"string"},{"name":"added","type":"long"},{"name":"comment","type":"string"},{"name":"commentLength","type":"long"},{"name":"isNew","type":"string"},{"name":"isMinor","type":"string"},{"name":"delta","type":"long"},{"name":"isAnonymous","type":"string"},{"name":"user","type":"string"},{"name":"deltaBucket","type":"long"},{"name":"deleted","type":"long"},{"name":"namespace","type":"string"},{"name":"cityName","type":"string"},{"name":"countryName","type":"string"},{"name":"regionIsoCode","type":"string"},{"name":"metroCode","type":"string"},{"name":"countryIsoCode","type":"string"},{"name":"regionName","type":"string"}]'
|
||||||
|
)
|
||||||
|
))
|
||||||
|
SELECT
|
||||||
|
--:ISSUE: The spec contained transforms that could not be automatically converted.
|
||||||
|
CASE WHEN CAST("timestamp" AS BIGINT) > 0 THEN MILLIS_TO_TIMESTAMP(CAST("timestamp" AS BIGINT)) ELSE TIME_PARSE("timestamp") END AS __time,
|
||||||
|
"isRobot",
|
||||||
|
"channel",
|
||||||
|
"flags",
|
||||||
|
"isUnpatrolled",
|
||||||
|
"page",
|
||||||
|
"diffUrl",
|
||||||
|
"added",
|
||||||
|
REWRITE_[_some_expression_]_TO_SQL AS "comment", --:ISSUE: Transform for dimension could not be converted
|
||||||
|
"commentLength",
|
||||||
|
"isNew",
|
||||||
|
"isMinor",
|
||||||
|
"delta",
|
||||||
|
"isAnonymous",
|
||||||
|
"user",
|
||||||
|
"deltaBucket",
|
||||||
|
"deleted",
|
||||||
|
"namespace",
|
||||||
|
"cityName",
|
||||||
|
"countryName",
|
||||||
|
"regionIsoCode",
|
||||||
|
"metroCode",
|
||||||
|
"countryIsoCode",
|
||||||
|
"regionName"
|
||||||
|
FROM source
|
||||||
|
WHERE REWRITE_[{"type":"strange"}]_TO_SQL --:ISSUE: The spec contained a filter that could not be automatically converted, please convert it manually
|
||||||
|
PARTITIONED BY HOUR
|
||||||
|
CLUSTERED BY "isRobot"
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -102,10 +102,19 @@ export function convertSpecToSql(spec: any): QueryWithContext {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transforms: Transform[] = deepGet(spec, 'spec.dataSchema.transformSpec.transforms') || [];
|
||||||
|
if (!Array.isArray(transforms)) {
|
||||||
|
throw new Error(`spec.dataSchema.transformSpec.transforms is not an array`);
|
||||||
|
}
|
||||||
|
|
||||||
let timeExpression: string;
|
let timeExpression: string;
|
||||||
const column = timestampSpec.column || 'timestamp';
|
const column = timestampSpec.column || 'timestamp';
|
||||||
const columnRef = SqlRef.column(column);
|
const columnRef = SqlRef.column(column);
|
||||||
const format = timestampSpec.format || 'auto';
|
const format = timestampSpec.format || 'auto';
|
||||||
|
const timeTransform = transforms.find(t => t.name === '__time');
|
||||||
|
if (timeTransform) {
|
||||||
|
timeExpression = `REWRITE_[${timeTransform.expression}]_TO_SQL`;
|
||||||
|
} else {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'auto':
|
case 'auto':
|
||||||
columns.unshift({ name: column, type: 'string' });
|
columns.unshift({ name: column, type: 'string' });
|
||||||
|
@ -142,6 +151,7 @@ export function convertSpecToSql(spec: any): QueryWithContext {
|
||||||
timeExpression = `TIME_PARSE(${columnRef}, ${SqlLiteral.create(format)})`;
|
timeExpression = `TIME_PARSE(${columnRef}, ${SqlLiteral.create(format)})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (timestampSpec.missingValue) {
|
if (timestampSpec.missingValue) {
|
||||||
timeExpression = `COALESCE(${timeExpression}, TIME_PARSE(${SqlLiteral.create(
|
timeExpression = `COALESCE(${timeExpression}, TIME_PARSE(${SqlLiteral.create(
|
||||||
|
@ -238,19 +248,24 @@ export function convertSpecToSql(spec: any): QueryWithContext {
|
||||||
|
|
||||||
lines.push(`SELECT`);
|
lines.push(`SELECT`);
|
||||||
|
|
||||||
const transforms: Transform[] = deepGet(spec, 'spec.dataSchema.transformSpec.transforms') || [];
|
|
||||||
if (!Array.isArray(transforms))
|
|
||||||
throw new Error(`spec.dataSchema.transformSpec.transforms is not an array`);
|
|
||||||
if (transforms.length) {
|
if (transforms.length) {
|
||||||
lines.push(` -- The spec contained transforms that could not be automatically converted.`);
|
lines.push(
|
||||||
|
` --:ISSUE: The spec contained transforms that could not be automatically converted.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dimensionExpressions = [` ${timeExpression} AS __time,`].concat(
|
const dimensionExpressions = [
|
||||||
|
` ${timeExpression} AS __time,${
|
||||||
|
timeTransform ? ` --:ISSUE: Transform for __time could not be converted` : ''
|
||||||
|
}`,
|
||||||
|
].concat(
|
||||||
dimensions.flatMap((dimension: DimensionSpec) => {
|
dimensions.flatMap((dimension: DimensionSpec) => {
|
||||||
const dimensionName = dimension.name;
|
const dimensionName = dimension.name;
|
||||||
const relevantTransform = transforms.find(t => t.name === dimensionName);
|
const relevantTransform = transforms.find(t => t.name === dimensionName);
|
||||||
return ` ${SqlRef.columnWithQuotes(dimensionName)},${
|
return ` ${
|
||||||
relevantTransform ? ` -- Relevant transform: ${JSONBig.stringify(relevantTransform)}` : ''
|
relevantTransform ? `REWRITE_[${relevantTransform.expression}]_TO_SQL AS ` : ''
|
||||||
|
}${SqlRef.columnWithQuotes(dimensionName)},${
|
||||||
|
relevantTransform ? ` --:ISSUE: Transform for dimension could not be converted` : ''
|
||||||
}`;
|
}`;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -275,9 +290,9 @@ export function convertSpecToSql(spec: any): QueryWithContext {
|
||||||
lines.push(`WHERE ${convertFilter(filter)}`);
|
lines.push(`WHERE ${convertFilter(filter)}`);
|
||||||
} catch {
|
} catch {
|
||||||
lines.push(
|
lines.push(
|
||||||
`-- The spec contained a filter that could not be automatically converted: ${JSONBig.stringify(
|
`WHERE REWRITE_[${JSONBig.stringify(
|
||||||
filter,
|
filter,
|
||||||
)}`,
|
)}]_TO_SQL --:ISSUE: The spec contained a filter that could not be automatically converted, please convert it manually`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import hasOwnProp from 'has-own-prop';
|
import hasOwnProp from 'has-own-prop';
|
||||||
|
|
||||||
// This is set to the latest available version and should be updated to the next version before release
|
// This is set to the latest available version and should be updated to the next version before release
|
||||||
const DRUID_DOCS_VERSION = '24.0.0';
|
const DRUID_DOCS_VERSION = '24.0.1';
|
||||||
|
|
||||||
function fillVersion(str: string): string {
|
function fillVersion(str: string): string {
|
||||||
return str.replace(/\{\{VERSION}}/g, DRUID_DOCS_VERSION);
|
return str.replace(/\{\{VERSION}}/g, DRUID_DOCS_VERSION);
|
||||||
|
@ -63,6 +63,7 @@ export type LinkNames =
|
||||||
| 'DOCS_SQL'
|
| 'DOCS_SQL'
|
||||||
| 'DOCS_RUNE'
|
| 'DOCS_RUNE'
|
||||||
| 'DOCS_API'
|
| 'DOCS_API'
|
||||||
|
| 'DOCS_MSQ_ERROR'
|
||||||
| 'COMMUNITY'
|
| 'COMMUNITY'
|
||||||
| 'SLACK'
|
| 'SLACK'
|
||||||
| 'USER_GROUP'
|
| 'USER_GROUP'
|
||||||
|
@ -82,6 +83,8 @@ export function getLink(linkName: LinkNames): string {
|
||||||
return `${links.docsHref}/querying/querying.html`;
|
return `${links.docsHref}/querying/querying.html`;
|
||||||
case 'DOCS_API':
|
case 'DOCS_API':
|
||||||
return `${links.docsHref}/operations/api-reference.html`;
|
return `${links.docsHref}/operations/api-reference.html`;
|
||||||
|
case 'DOCS_MSQ_ERROR':
|
||||||
|
return `${links.docsHref}/multi-stage-query/concepts.html#error-codes`;
|
||||||
case 'COMMUNITY':
|
case 'COMMUNITY':
|
||||||
return links.communityHref;
|
return links.communityHref;
|
||||||
case 'SLACK':
|
case 'SLACK':
|
||||||
|
|
|
@ -43,7 +43,7 @@ export function GenericFilterInput({ column, filter, onChange, key }: FilterRend
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [focused, setFocused] = useState(false);
|
const [focused, setFocused] = useState(false);
|
||||||
|
|
||||||
const disableComparisons = String(column.headerClassName).includes('disable-comparisons');
|
const enableComparisons = String(column.headerClassName).includes('enable-comparisons');
|
||||||
|
|
||||||
const { mode, needle } = (filter ? parseFilterModeAndNeedle(filter, true) : undefined) || {
|
const { mode, needle } = (filter ? parseFilterModeAndNeedle(filter, true) : undefined) || {
|
||||||
mode: '~',
|
mode: '~',
|
||||||
|
@ -64,7 +64,7 @@ export function GenericFilterInput({ column, filter, onChange, key }: FilterRend
|
||||||
onInteraction={setMenuOpen}
|
onInteraction={setMenuOpen}
|
||||||
content={
|
content={
|
||||||
<Menu>
|
<Menu>
|
||||||
{(disableComparisons ? FILTER_MODES_NO_COMPARISON : FILTER_MODES).map((m, i) => (
|
{(enableComparisons ? FILTER_MODES : FILTER_MODES_NO_COMPARISON).map((m, i) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={i}
|
key={i}
|
||||||
icon={filterModeToIcon(m)}
|
icon={filterModeToIcon(m)}
|
||||||
|
|
|
@ -40,6 +40,16 @@ export function nonEmptyArray(a: any): a is unknown[] {
|
||||||
return Array.isArray(a) && Boolean(a.length);
|
return Array.isArray(a) && Boolean(a.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSimpleArray(a: any): a is (string | number | boolean)[] {
|
||||||
|
return (
|
||||||
|
Array.isArray(a) &&
|
||||||
|
a.every(x => {
|
||||||
|
const t = typeof x;
|
||||||
|
return t === 'string' || t === 'number' || t === 'boolean';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function wait(ms: number): Promise<void> {
|
export function wait(ms: number): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(resolve, ms);
|
setTimeout(resolve, ms);
|
||||||
|
|
|
@ -16,12 +16,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Callout, Code, FormGroup } from '@blueprintjs/core';
|
import { Button, Callout, Code, FormGroup, Intent } from '@blueprintjs/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ExternalLink, LearnMore } from '../../components';
|
import { ExternalLink, LearnMore } from '../../components';
|
||||||
import { DimensionMode, getIngestionDocLink, IngestionSpec } from '../../druid-models';
|
import { DimensionMode, getIngestionDocLink, IngestionSpec } from '../../druid-models';
|
||||||
import { getLink } from '../../links';
|
import { getLink } from '../../links';
|
||||||
|
import { deepGet, deepSet } from '../../utils';
|
||||||
|
|
||||||
export interface ConnectMessageProps {
|
export interface ConnectMessageProps {
|
||||||
inlineMode: boolean;
|
inlineMode: boolean;
|
||||||
|
@ -216,3 +217,48 @@ export const SpecMessage = React.memo(function SpecMessage() {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface AppendToExistingIssueProps {
|
||||||
|
spec: Partial<IngestionSpec>;
|
||||||
|
onChangeSpec(newSpec: Partial<IngestionSpec>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppendToExistingIssue = React.memo(function AppendToExistingIssue(
|
||||||
|
props: AppendToExistingIssueProps,
|
||||||
|
) {
|
||||||
|
const { spec, onChangeSpec } = props;
|
||||||
|
|
||||||
|
const partitionsSpecType = deepGet(spec, 'spec.tuningConfig.partitionsSpec.type');
|
||||||
|
if (
|
||||||
|
partitionsSpecType === 'dynamic' ||
|
||||||
|
deepGet(spec, 'spec.ioConfig.appendToExisting') !== true
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicPartitionSpec = {
|
||||||
|
type: 'dynamic',
|
||||||
|
maxRowsPerSegment:
|
||||||
|
deepGet(spec, 'spec.tuningConfig.partitionsSpec.maxRowsPerSegment') ||
|
||||||
|
deepGet(spec, 'spec.tuningConfig.partitionsSpec.targetRowsPerSegment'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup>
|
||||||
|
<Callout intent={Intent.DANGER}>
|
||||||
|
<p>
|
||||||
|
Only <Code>dynamic</Code> partitioning supports <Code>appendToExisting: true</Code>. You
|
||||||
|
have currently selected <Code>{partitionsSpecType}</Code> partitioning.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
intent={Intent.SUCCESS}
|
||||||
|
onClick={() =>
|
||||||
|
onChangeSpec(deepSet(spec, 'spec.tuningConfig.partitionsSpec', dynamicPartitionSpec))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Change to <Code>dynamic</Code> partitioning
|
||||||
|
</Button>
|
||||||
|
</Callout>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -168,6 +168,7 @@ import { ExamplePicker } from './example-picker/example-picker';
|
||||||
import { FilterTable, filterTableSelectedColumnName } from './filter-table/filter-table';
|
import { FilterTable, filterTableSelectedColumnName } from './filter-table/filter-table';
|
||||||
import { FormEditor } from './form-editor/form-editor';
|
import { FormEditor } from './form-editor/form-editor';
|
||||||
import {
|
import {
|
||||||
|
AppendToExistingIssue,
|
||||||
ConnectMessage,
|
ConnectMessage,
|
||||||
FilterMessage,
|
FilterMessage,
|
||||||
ParserMessage,
|
ParserMessage,
|
||||||
|
@ -1490,7 +1491,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
if (canFlatten && !flattenFields.length && parserQueryState.data) {
|
if (canFlatten && !flattenFields.length && parserQueryState.data) {
|
||||||
suggestedFlattenFields = computeFlattenPathsForData(
|
suggestedFlattenFields = computeFlattenPathsForData(
|
||||||
filterMap(parserQueryState.data.rows, r => r.input),
|
filterMap(parserQueryState.data.rows, r => r.input),
|
||||||
'path',
|
|
||||||
'ignore-arrays',
|
'ignore-arrays',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3003,6 +3003,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<PartitionMessage />
|
<PartitionMessage />
|
||||||
{nonsensicalSingleDimPartitioningMessage}
|
{nonsensicalSingleDimPartitioningMessage}
|
||||||
|
<AppendToExistingIssue spec={spec} onChangeSpec={this.updateSpec} />
|
||||||
</div>
|
</div>
|
||||||
{this.renderNextBar({
|
{this.renderNextBar({
|
||||||
disabled: invalidPartitionConfig(spec),
|
disabled: invalidPartitionConfig(spec),
|
||||||
|
@ -3096,8 +3097,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
label: 'Append to existing',
|
label: 'Append to existing',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
defined: spec =>
|
// appendToExisting can only be set on 'dynamic' portioning.
|
||||||
deepGet(spec, 'spec.tuningConfig.partitionsSpec.type') === 'dynamic',
|
// We chose to show it always and instead have a specific message, separate from this form, to notify the user of the issue.
|
||||||
info: (
|
info: (
|
||||||
<>
|
<>
|
||||||
Creates segments as additional shards of the latest version, effectively
|
Creates segments as additional shards of the latest version, effectively
|
||||||
|
@ -3166,6 +3167,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
</div>
|
</div>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<PublishMessage />
|
<PublishMessage />
|
||||||
|
<AppendToExistingIssue spec={spec} onChangeSpec={this.updateSpec} />
|
||||||
</div>
|
</div>
|
||||||
{this.renderNextBar({})}
|
{this.renderNextBar({})}
|
||||||
</>
|
</>
|
||||||
|
@ -3234,6 +3236,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
>{`There is an issue with the spec: ${issueWithSpec}`}</Callout>
|
>{`There is an issue with the spec: ${issueWithSpec}`}</Callout>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
|
<AppendToExistingIssue spec={spec} onChangeSpec={this.updateSpec} />
|
||||||
</div>
|
</div>
|
||||||
<div className="next-bar">
|
<div className="next-bar">
|
||||||
{!isEmptyIngestionSpec(spec) && (
|
{!isEmptyIngestionSpec(spec) && (
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { ResizeEntry } from '@blueprintjs/core';
|
||||||
import { ResizeSensor2 } from '@blueprintjs/popover2';
|
import { ResizeSensor2 } from '@blueprintjs/popover2';
|
||||||
import type { Ace } from 'ace-builds';
|
import type { Ace } from 'ace-builds';
|
||||||
import ace from 'ace-builds';
|
import ace from 'ace-builds';
|
||||||
|
import { SqlRef, SqlTableRef } from 'druid-query-toolkit';
|
||||||
import escape from 'lodash.escape';
|
import escape from 'lodash.escape';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AceEditor from 'react-ace';
|
import AceEditor from 'react-ace';
|
||||||
|
@ -150,7 +151,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
|
||||||
) {
|
) {
|
||||||
const completions = ([] as any[]).concat(
|
const completions = ([] as any[]).concat(
|
||||||
uniq(columnMetadata.map(d => d.TABLE_SCHEMA)).map(v => ({
|
uniq(columnMetadata.map(d => d.TABLE_SCHEMA)).map(v => ({
|
||||||
value: v,
|
value: SqlTableRef.create(v).toString(),
|
||||||
score: 10,
|
score: 10,
|
||||||
meta: 'schema',
|
meta: 'schema',
|
||||||
})),
|
})),
|
||||||
|
@ -159,7 +160,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
|
||||||
.filter(d => (currentSchema ? d.TABLE_SCHEMA === currentSchema : true))
|
.filter(d => (currentSchema ? d.TABLE_SCHEMA === currentSchema : true))
|
||||||
.map(d => d.TABLE_NAME),
|
.map(d => d.TABLE_NAME),
|
||||||
).map(v => ({
|
).map(v => ({
|
||||||
value: v,
|
value: SqlTableRef.create(v).toString(),
|
||||||
score: 49,
|
score: 49,
|
||||||
meta: 'datasource',
|
meta: 'datasource',
|
||||||
})),
|
})),
|
||||||
|
@ -172,7 +173,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
|
||||||
)
|
)
|
||||||
.map(d => d.COLUMN_NAME),
|
.map(d => d.COLUMN_NAME),
|
||||||
).map(v => ({
|
).map(v => ({
|
||||||
value: v,
|
value: SqlRef.column(v).toString(),
|
||||||
score: 50,
|
score: 50,
|
||||||
meta: 'column',
|
meta: 'column',
|
||||||
})),
|
})),
|
||||||
|
|
|
@ -169,6 +169,7 @@ exports[`SegmentsView matches snapshot 1`] = `
|
||||||
"accessor": "start",
|
"accessor": "start",
|
||||||
"defaultSortDesc": true,
|
"defaultSortDesc": true,
|
||||||
"filterable": true,
|
"filterable": true,
|
||||||
|
"headerClassName": "enable-comparisons",
|
||||||
"show": true,
|
"show": true,
|
||||||
"sortable": true,
|
"sortable": true,
|
||||||
"width": 160,
|
"width": 160,
|
||||||
|
@ -179,6 +180,7 @@ exports[`SegmentsView matches snapshot 1`] = `
|
||||||
"accessor": "end",
|
"accessor": "end",
|
||||||
"defaultSortDesc": true,
|
"defaultSortDesc": true,
|
||||||
"filterable": true,
|
"filterable": true,
|
||||||
|
"headerClassName": "enable-comparisons",
|
||||||
"show": true,
|
"show": true,
|
||||||
"sortable": true,
|
"sortable": true,
|
||||||
"width": 160,
|
"width": 160,
|
||||||
|
@ -206,7 +208,6 @@ exports[`SegmentsView matches snapshot 1`] = `
|
||||||
"Cell": [Function],
|
"Cell": [Function],
|
||||||
"Header": "Shard type",
|
"Header": "Shard type",
|
||||||
"accessor": [Function],
|
"accessor": [Function],
|
||||||
"headerClassName": "disable-comparisons",
|
|
||||||
"id": "shard_type",
|
"id": "shard_type",
|
||||||
"show": true,
|
"show": true,
|
||||||
"sortable": false,
|
"sortable": false,
|
||||||
|
|
|
@ -485,7 +485,7 @@ END AS "time_span"`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderFilterableCell(field: string, disableComparisons = false) {
|
private renderFilterableCell(field: string, enableComparisons = false) {
|
||||||
const { segmentFilter } = this.state;
|
const { segmentFilter } = this.state;
|
||||||
|
|
||||||
return (row: { value: any }) => (
|
return (row: { value: any }) => (
|
||||||
|
@ -494,7 +494,7 @@ END AS "time_span"`,
|
||||||
value={row.value}
|
value={row.value}
|
||||||
filters={segmentFilter}
|
filters={segmentFilter}
|
||||||
onFiltersChange={filters => this.setState({ segmentFilter: filters })}
|
onFiltersChange={filters => this.setState({ segmentFilter: filters })}
|
||||||
disableComparisons={disableComparisons}
|
enableComparisons={enableComparisons}
|
||||||
>
|
>
|
||||||
{row.value}
|
{row.value}
|
||||||
</TableFilterableCell>
|
</TableFilterableCell>
|
||||||
|
@ -582,21 +582,23 @@ END AS "time_span"`,
|
||||||
Header: 'Start',
|
Header: 'Start',
|
||||||
show: visibleColumns.shown('Start'),
|
show: visibleColumns.shown('Start'),
|
||||||
accessor: 'start',
|
accessor: 'start',
|
||||||
|
headerClassName: 'enable-comparisons',
|
||||||
width: 160,
|
width: 160,
|
||||||
sortable: hasSql,
|
sortable: hasSql,
|
||||||
defaultSortDesc: true,
|
defaultSortDesc: true,
|
||||||
filterable: allowGeneralFilter,
|
filterable: allowGeneralFilter,
|
||||||
Cell: this.renderFilterableCell('start'),
|
Cell: this.renderFilterableCell('start', true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'End',
|
Header: 'End',
|
||||||
show: visibleColumns.shown('End'),
|
show: visibleColumns.shown('End'),
|
||||||
accessor: 'end',
|
accessor: 'end',
|
||||||
|
headerClassName: 'enable-comparisons',
|
||||||
width: 160,
|
width: 160,
|
||||||
sortable: hasSql,
|
sortable: hasSql,
|
||||||
defaultSortDesc: true,
|
defaultSortDesc: true,
|
||||||
filterable: allowGeneralFilter,
|
filterable: allowGeneralFilter,
|
||||||
Cell: this.renderFilterableCell('end'),
|
Cell: this.renderFilterableCell('end', true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Version',
|
Header: 'Version',
|
||||||
|
@ -623,7 +625,6 @@ END AS "time_span"`,
|
||||||
id: 'shard_type',
|
id: 'shard_type',
|
||||||
width: 100,
|
width: 100,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
headerClassName: 'disable-comparisons',
|
|
||||||
accessor: d => {
|
accessor: d => {
|
||||||
let v: any;
|
let v: any;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -8,7 +8,12 @@ exports[`ExecutionErrorPane matches snapshot 1`] = `
|
||||||
<p
|
<p
|
||||||
className="error-message-text"
|
className="error-message-text"
|
||||||
>
|
>
|
||||||
TooManyWarnings:
|
<Memo(ExternalLink)
|
||||||
|
href="https://druid.apache.org/docs/24.0.1/multi-stage-query/concepts.html#error-codes"
|
||||||
|
>
|
||||||
|
TooManyWarnings
|
||||||
|
</Memo(ExternalLink)>
|
||||||
|
:
|
||||||
Too many warnings of type CannotParseExternalData generated (max = 10)
|
Too many warnings of type CannotParseExternalData generated (max = 10)
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -20,9 +20,10 @@ import { Callout } from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { ClickToCopy } from '../../../components';
|
import { ClickToCopy, ExternalLink } from '../../../components';
|
||||||
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
|
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
|
||||||
import { Execution } from '../../../druid-models';
|
import { Execution } from '../../../druid-models';
|
||||||
|
import { getLink } from '../../../links';
|
||||||
import { downloadQueryDetailArchive } from '../../../utils';
|
import { downloadQueryDetailArchive } from '../../../utils';
|
||||||
|
|
||||||
import './execution-error-pane.scss';
|
import './execution-error-pane.scss';
|
||||||
|
@ -43,7 +44,12 @@ export const ExecutionErrorPane = React.memo(function ExecutionErrorPane(
|
||||||
return (
|
return (
|
||||||
<Callout className="execution-error-pane" icon={IconNames.ERROR}>
|
<Callout className="execution-error-pane" icon={IconNames.ERROR}>
|
||||||
<p className="error-message-text">
|
<p className="error-message-text">
|
||||||
{error.errorCode && <>{`${error.errorCode}: `}</>}
|
{error.errorCode && (
|
||||||
|
<>
|
||||||
|
<ExternalLink href={getLink('DOCS_MSQ_ERROR')}>{error.errorCode}</ExternalLink>
|
||||||
|
{': '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{error.errorMessage || (exceptionStackTrace || '').split('\n')[0]}
|
{error.errorMessage || (exceptionStackTrace || '').split('\n')[0]}
|
||||||
{exceptionStackTrace && (
|
{exceptionStackTrace && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { ResizeSensor2 } from '@blueprintjs/popover2';
|
||||||
import type { Ace } from 'ace-builds';
|
import type { Ace } from 'ace-builds';
|
||||||
import ace from 'ace-builds';
|
import ace from 'ace-builds';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { SqlRef, SqlTableRef } from 'druid-query-toolkit';
|
||||||
import escape from 'lodash.escape';
|
import escape from 'lodash.escape';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AceEditor from 'react-ace';
|
import AceEditor from 'react-ace';
|
||||||
|
@ -163,7 +164,7 @@ export class FlexibleQueryInput extends React.PureComponent<
|
||||||
) {
|
) {
|
||||||
const completions = ([] as any[]).concat(
|
const completions = ([] as any[]).concat(
|
||||||
uniq(columnMetadata.map(d => d.TABLE_SCHEMA)).map(v => ({
|
uniq(columnMetadata.map(d => d.TABLE_SCHEMA)).map(v => ({
|
||||||
value: v,
|
value: SqlTableRef.create(v).toString(),
|
||||||
score: 10,
|
score: 10,
|
||||||
meta: 'schema',
|
meta: 'schema',
|
||||||
})),
|
})),
|
||||||
|
@ -172,7 +173,7 @@ export class FlexibleQueryInput extends React.PureComponent<
|
||||||
.filter(d => (currentSchema ? d.TABLE_SCHEMA === currentSchema : true))
|
.filter(d => (currentSchema ? d.TABLE_SCHEMA === currentSchema : true))
|
||||||
.map(d => d.TABLE_NAME),
|
.map(d => d.TABLE_NAME),
|
||||||
).map(v => ({
|
).map(v => ({
|
||||||
value: v,
|
value: SqlTableRef.create(v).toString(),
|
||||||
score: 49,
|
score: 49,
|
||||||
meta: 'datasource',
|
meta: 'datasource',
|
||||||
})),
|
})),
|
||||||
|
@ -185,7 +186,7 @@ export class FlexibleQueryInput extends React.PureComponent<
|
||||||
)
|
)
|
||||||
.map(d => d.COLUMN_NAME),
|
.map(d => d.COLUMN_NAME),
|
||||||
).map(v => ({
|
).map(v => ({
|
||||||
value: v,
|
value: SqlRef.column(v).toString(),
|
||||||
score: 50,
|
score: 50,
|
||||||
meta: 'column',
|
meta: 'column',
|
||||||
})),
|
})),
|
||||||
|
|
|
@ -82,7 +82,7 @@ function jsonValue(ex: SqlExpression, path: string): SqlExpression {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJsonPaths(jsons: Record<string, any>[]): string[] {
|
function getJsonPaths(jsons: Record<string, any>[]): string[] {
|
||||||
return ['$.'].concat(computeFlattenExprsForData(jsons, 'path', 'include-arrays', true));
|
return ['$.'].concat(computeFlattenExprsForData(jsons, 'include-arrays', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isComparable(x: unknown): boolean {
|
function isComparable(x: unknown): boolean {
|
||||||
|
|
|
@ -71,6 +71,6 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="console-config.js"></script>
|
<script src="console-config.js"></script>
|
||||||
<script src="public/web-console-24.0.0.js"></script>
|
<script src="public/web-console-24.0.1.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue