This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new d295b9158f Web console: dynamic query parameters UI (#14921)
d295b9158f is described below
commit d295b9158ff12800bd33213559f20b672d7c2c87
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Tue Aug 29 23:14:25 2023 -0700
Web console: dynamic query parameters UI (#14921)
* fix nvl in table
* add query parameter dialog
* pre-wrap in the tables
* fix typo
---
.../fancy-numeric-input/fancy-numeric-input.tsx | 9 +-
.../src/components/table-cell/table-cell.scss | 1 +
.../kill-datasource-dialog.tsx | 2 +-
.../workbench-query/workbench-query.ts | 22 +-
web-console/src/utils/index.tsx | 1 -
web-console/src/utils/query-cursor.ts | 55 ---
web-console/src/utils/sql.ts | 5 +
web-console/src/utils/types.ts | 3 +
.../views/datasources-view/datasources-view.tsx | 4 +-
.../explore-view/modules/table-react-module.tsx | 47 +--
.../string-menu-items/string-menu-items.tsx | 12 +-
.../query-parameters-dialog.spec.tsx.snap | 457 +++++++++++++++++++++
.../query-parameters-dialog.scss} | 38 +-
.../query-parameters-dialog.spec.tsx} | 52 +--
.../query-parameters-dialog.tsx | 152 +++++++
.../views/workbench-view/run-panel/run-panel.tsx | 18 +
16 files changed, 714 insertions(+), 164 deletions(-)
diff --git
a/web-console/src/components/fancy-numeric-input/fancy-numeric-input.tsx
b/web-console/src/components/fancy-numeric-input/fancy-numeric-input.tsx
index ad38196cae..de6bc51070 100644
--- a/web-console/src/components/fancy-numeric-input/fancy-numeric-input.tsx
+++ b/web-console/src/components/fancy-numeric-input/fancy-numeric-input.tsx
@@ -80,6 +80,7 @@ export interface FancyNumericInputProps {
minorStepSize?: number;
stepSize?: number;
majorStepSize?: number;
+ arbitraryPrecision?: boolean;
}
export const FancyNumericInput = React.memo(function FancyNumericInput(
@@ -103,6 +104,7 @@ export const FancyNumericInput = React.memo(function
FancyNumericInput(
min,
max,
+ arbitraryPrecision,
} = props;
const stepSize = props.stepSize || 1;
@@ -110,8 +112,11 @@ export const FancyNumericInput = React.memo(function
FancyNumericInput(
const majorStepSize = props.majorStepSize || stepSize * 10;
function roundAndClamp(n: number): number {
- const inv = 1 / minorStepSize;
- return clamp(Math.floor(n * inv) / inv, min, max);
+ if (!arbitraryPrecision) {
+ const inv = 1 / minorStepSize;
+ n = Math.floor(n * inv) / inv;
+ }
+ return clamp(n, min, max);
}
const effectiveValue = value ?? defaultValue;
diff --git a/web-console/src/components/table-cell/table-cell.scss
b/web-console/src/components/table-cell/table-cell.scss
index 62c80d4663..03eef53a99 100644
--- a/web-console/src/components/table-cell/table-cell.scss
+++ b/web-console/src/components/table-cell/table-cell.scss
@@ -20,6 +20,7 @@
.table-cell {
padding: $table-cell-v-padding $table-cell-h-padding;
+ white-space: pre;
&.null,
&.empty {
diff --git
a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
index 3eb7e9fdf2..f95a5a5d3b 100644
--- a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
+++ b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
@@ -92,7 +92,7 @@ export const KillDatasourceDialog = function
KillDatasourceDialog(
format.
</p>
<p>
- If you have streaming ingestion running make sure that your
interval range doe not
+ If you have streaming ingestion running make sure that your
interval range does not
overlap with intervals where streaming data is being added -
otherwise the kill task
will not start.
</p>
diff --git a/web-console/src/druid-models/workbench-query/workbench-query.ts
b/web-console/src/druid-models/workbench-query/workbench-query.ts
index 6fe223847f..d7847287fa 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query.ts
@@ -17,6 +17,7 @@
*/
import type {
+ QueryParameter,
SqlClusteredByClause,
SqlExpression,
SqlPartitionedByClause,
@@ -66,6 +67,7 @@ interface IngestionLines {
export interface WorkbenchQueryValue {
queryString: string;
queryContext: QueryContext;
+ queryParameters?: QueryParameter[];
engine?: DruidEngine;
lastExecution?: LastExecution;
unlimited?: boolean;
@@ -235,6 +237,7 @@ export class WorkbenchQuery {
public readonly queryString: string;
public readonly queryContext: QueryContext;
+ public readonly queryParameters?: QueryParameter[];
public readonly engine?: DruidEngine;
public readonly lastExecution?: LastExecution;
public readonly unlimited?: boolean;
@@ -251,6 +254,7 @@ export class WorkbenchQuery {
}
this.queryString = queryString;
this.queryContext = value.queryContext;
+ this.queryParameters = value.queryParameters;
// Start back compat code for the engine names that might be coming from
local storage
let possibleEngine: string | undefined = value.engine;
@@ -274,6 +278,7 @@ export class WorkbenchQuery {
return {
queryString: this.queryString,
queryContext: this.queryContext,
+ queryParameters: this.queryParameters,
engine: this.engine,
unlimited: this.unlimited,
};
@@ -297,6 +302,10 @@ export class WorkbenchQuery {
return new WorkbenchQuery({ ...this.valueOf(), queryContext });
}
+ public changeQueryParameters(queryParameters: QueryParameter[] | undefined):
WorkbenchQuery {
+ return new WorkbenchQuery({ ...this.valueOf(), queryParameters });
+ }
+
public changeEngine(engine: DruidEngine | undefined): WorkbenchQuery {
return new WorkbenchQuery({ ...this.valueOf(), engine });
}
@@ -425,11 +434,12 @@ export class WorkbenchQuery {
let ret: WorkbenchQuery = this;
// Explicitly select MSQ, adjust the context, set maxNumTasks to the
lowest possible and add in ingest mode flags
+ const { queryContext } = this;
ret = ret.changeEngine('sql-msq-task').changeQueryContext({
- ...this.queryContext,
+ ...queryContext,
maxNumTasks: 2,
- finalizeAggregations: false,
- groupByEnableMultiValueUnnesting: false,
+ finalizeAggregations: queryContext.finalizeAggregations ?? false,
+ groupByEnableMultiValueUnnesting:
queryContext.groupByEnableMultiValueUnnesting ?? false,
});
// Remove everything pertaining to INSERT INTO / REPLACE INTO from the
query string
@@ -458,7 +468,7 @@ export class WorkbenchQuery {
prefixLines: number;
cancelQueryId?: string;
} {
- const { queryString, queryContext, unlimited, prefixLines } = this;
+ const { queryString, queryContext, queryParameters, unlimited, prefixLines
} = this;
const engine = this.getEffectiveEngine();
if (engine === 'native') {
@@ -544,6 +554,10 @@ export class WorkbenchQuery {
apiQuery.context.groupByEnableMultiValueUnnesting ??= !ingestQuery;
}
+ if (Array.isArray(queryParameters) && queryParameters.length) {
+ apiQuery.parameters = queryParameters;
+ }
+
return {
engine,
query: apiQuery,
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index c27a070b97..e7dc33e7a6 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -29,7 +29,6 @@ export * from './local-storage-backed-visibility';
export * from './local-storage-keys';
export * from './object-change';
export * from './query-action';
-export * from './query-cursor';
export * from './query-manager';
export * from './query-state';
export * from './sample-query';
diff --git a/web-console/src/utils/query-cursor.ts
b/web-console/src/utils/query-cursor.ts
deleted file mode 100644
index 94bbe17938..0000000000
--- a/web-console/src/utils/query-cursor.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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 type { SqlBase, SqlQuery } from '@druid-toolkit/query';
-import { L } from '@druid-toolkit/query';
-
-import type { RowColumn } from './general';
-
-export const EMPTY_LITERAL = L('');
-
-const CRAZY_STRING = '[email protected].$';
-const DOT_DOT_DOT_LITERAL = L('...');
-
-export function prettyPrintSql(b: SqlBase): string {
- return b
- .walk(b => {
- if (b === EMPTY_LITERAL) {
- return DOT_DOT_DOT_LITERAL;
- }
- return b;
- })
- .prettyTrim(50)
- .toString();
-}
-
-export function findEmptyLiteralPosition(query: SqlQuery): RowColumn |
undefined {
- const subQueryString = query.walk(b => (b === EMPTY_LITERAL ?
L(CRAZY_STRING) : b)).toString();
-
- const crazyIndex = subQueryString.indexOf(CRAZY_STRING);
- if (crazyIndex < 0) return;
-
- const prefix = subQueryString.slice(0, crazyIndex);
- const lines = prefix.split(/\n/g);
- const row = lines.length - 1;
- const lastLine = lines[row];
- return {
- row: row,
- column: lastLine.length,
- };
-}
diff --git a/web-console/src/utils/sql.ts b/web-console/src/utils/sql.ts
index 7404ee3137..61cd4c7ed4 100644
--- a/web-console/src/utils/sql.ts
+++ b/web-console/src/utils/sql.ts
@@ -16,6 +16,7 @@
* limitations under the License.
*/
+import type { SqlBase } from '@druid-toolkit/query';
import {
SqlColumn,
SqlExpression,
@@ -28,6 +29,10 @@ import {
import type { RowColumn } from './general';
import { offsetToRowColumn } from './general';
+export function prettyPrintSql(b: SqlBase): string {
+ return b.prettyTrim(50).toString();
+}
+
export function timeFormatToSql(timeFormat: string): SqlExpression | undefined
{
switch (timeFormat) {
case 'auto':
diff --git a/web-console/src/utils/types.ts b/web-console/src/utils/types.ts
index ed192eff6e..d164d46138 100644
--- a/web-console/src/utils/types.ts
+++ b/web-console/src/utils/types.ts
@@ -89,6 +89,9 @@ export function dataTypeToIcon(dataType: string): IconName {
case 'COMPLEX<IPPREFIX>':
return IconNames.IP_ADDRESS;
+ case 'COMPLEX<SERIALIZABLEPAIRLONGSTRING>':
+ return IconNames.DOUBLE_CHEVRON_RIGHT;
+
case 'NULL':
return IconNames.CIRCLE;
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx
b/web-console/src/views/datasources-view/datasources-view.tsx
index 570a55a87b..75541b8299 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -383,8 +383,8 @@ export class DatasourcesView extends React.PureComponent<
return `SELECT
${columns.join(',\n')}
FROM sys.segments
-GROUP BY 1
-ORDER BY 1`;
+GROUP BY datasource
+ORDER BY datasource`;
}
static RUNNING_TASK_SQL = `SELECT
diff --git a/web-console/src/views/explore-view/modules/table-react-module.tsx
b/web-console/src/views/explore-view/modules/table-react-module.tsx
index 459e28014a..dabe6217c9 100644
--- a/web-console/src/views/explore-view/modules/table-react-module.tsx
+++ b/web-console/src/views/explore-view/modules/table-react-module.tsx
@@ -16,12 +16,11 @@
* limitations under the License.
*/
-import type { SqlOrderByExpression } from '@druid-toolkit/query';
+import type { SqlColumn, SqlOrderByExpression } from '@druid-toolkit/query';
import {
C,
F,
SqlCase,
- SqlColumn,
SqlExpression,
SqlFunction,
SqlLiteral,
@@ -80,22 +79,24 @@ function nullableColumn(column: ExpressionMeta) {
}
function nvl(ex: SqlExpression): SqlExpression {
- return SqlFunction.simple('NVL', [ex, NULL_REPLACEMENT]);
+ return SqlFunction.simple('NVL', [ex.cast('VARCHAR'), NULL_REPLACEMENT]);
}
-function nullif(ex: SqlExpression): SqlExpression {
- return SqlFunction.simple('NULLIF', [ex, NULL_REPLACEMENT]);
+function joinEquals(c1: SqlColumn, c2: SqlColumn, nullable: boolean):
SqlExpression {
+ return c1.applyIf(nullable, nvl).equal(c2.applyIf(nullable, nvl));
}
function toGroupByExpression(
splitColumn: ExpressionMeta,
- nvlIfNeeded: boolean,
timeBucket: string,
+ compareShiftDuration?: string,
) {
const { expression, sqlType, name } = splitColumn;
return expression
- .applyIf(sqlType === 'TIMESTAMP', e => SqlFunction.simple('TIME_FLOOR',
[e, timeBucket]))
- .applyIf(nvlIfNeeded && nullableColumn(splitColumn), nvl)
+ .applyIf(sqlType === 'TIMESTAMP' && compareShiftDuration, e =>
+ F.timeShift(e, compareShiftDuration!, 1),
+ )
+ .applyIf(sqlType === 'TIMESTAMP', e => F.timeFloor(e, timeBucket))
.as(name);
}
@@ -143,16 +144,6 @@ function toShowColumnExpression(
return ex.as(showColumn.name);
}
-function shiftTime(ex: SqlQuery, period: string): SqlQuery {
- return ex.walk(q => {
- if (q instanceof SqlColumn && q.getName() === '__time') {
- return SqlFunction.simple('TIME_SHIFT', [q, period, 1]);
- } else {
- return q;
- }
- }) as SqlQuery;
-}
-
interface QueryAndHints {
query: SqlQuery;
groupHints: string[];
@@ -327,7 +318,7 @@ function TableModule(props: TableModuleProps) {
const mainQuery = getInitQuery(table, where)
.applyForEach(splitColumns, (q, splitColumn) =>
- q.addSelect(toGroupByExpression(splitColumn, hasCompare, timeBucket), {
+ q.addSelect(toGroupByExpression(splitColumn, timeBucket), {
addToGroupBy: 'end',
}),
)
@@ -381,26 +372,20 @@ function TableModule(props: TableModuleProps) {
`compare${i}`,
getInitQuery(table, where)
.applyForEach(splitColumns, (q, splitColumn) =>
- q.addSelect(toGroupByExpression(splitColumn, true,
timeBucket), {
+ q.addSelect(toGroupByExpression(splitColumn, timeBucket,
comparePeriod), {
addToGroupBy: 'end',
}),
)
.applyForEach(metrics, (q, metric) =>
q.addSelect(metric.expression.as(metric.name)),
- )
- .apply(q => shiftTime(q, comparePeriod)),
+ ),
),
),
),
)
.changeSelectExpressions(
splitColumns
- .map(splitColumn =>
- main
- .column(splitColumn.name)
- .applyIf(nullableColumn(splitColumn), nullif)
- .as(splitColumn.name),
- )
+ .map(splitColumn =>
main.column(splitColumn.name).as(splitColumn.name))
.concat(
showColumns.map(showColumn =>
main.column(showColumn.name).as(showColumn.name)),
metrics.map(metric => main.column(metric.name).as(metric.name)),
@@ -432,7 +417,11 @@ function TableModule(props: TableModuleProps) {
T(`compare${i}`),
SqlExpression.and(
...splitColumns.map(splitColumn =>
-
main.column(splitColumn.name).equal(T(`compare${i}`).column(splitColumn.name)),
+ joinEquals(
+ main.column(splitColumn.name),
+ T(`compare${i}`).column(splitColumn.name),
+ nullableColumn(splitColumn),
+ ),
),
),
),
diff --git
a/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
b/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
index db566765d3..a32db7f8b6 100644
---
a/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
+++
b/web-console/src/views/workbench-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
@@ -19,11 +19,11 @@
import { MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { SqlExpression, SqlQuery } from '@druid-toolkit/query';
-import { C, F, N, SqlJoinPart, T } from '@druid-toolkit/query';
+import { C, F, N, SqlJoinPart, SqlPlaceholder, T } from '@druid-toolkit/query';
import type { JSX } from 'react';
import React from 'react';
-import { EMPTY_LITERAL, prettyPrintSql } from '../../../../../utils';
+import { prettyPrintSql } from '../../../../../utils';
import { getJoinColumns } from '../../column-tree';
export interface StringMenuItemsProps {
@@ -53,9 +53,9 @@ export const StringMenuItems = React.memo(function
StringMenuItems(props: String
return (
<MenuItem icon={IconNames.FILTER} text="Filter">
{filterMenuItem(column.isNotNull())}
- {filterMenuItem(column.equal(EMPTY_LITERAL), false)}
- {filterMenuItem(column.like(EMPTY_LITERAL), false)}
- {filterMenuItem(F('REGEXP_LIKE', column, EMPTY_LITERAL), false)}
+ {filterMenuItem(column.equal(SqlPlaceholder.PLACEHOLDER), false)}
+ {filterMenuItem(column.like(SqlPlaceholder.PLACEHOLDER), false)}
+ {filterMenuItem(F('REGEXP_LIKE', column, SqlPlaceholder.PLACEHOLDER),
false)}
</MenuItem>
);
}
@@ -136,7 +136,7 @@ export const StringMenuItems = React.memo(function
StringMenuItems(props: String
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
{aggregateMenuItem(F.countDistinct(column), `dist_${columnName}`)}
{aggregateMenuItem(
- F.count().addWhereExpression(column.equal(EMPTY_LITERAL)),
+
F.count().addWhereExpression(column.equal(SqlPlaceholder.PLACEHOLDER)),
`filtered_dist_${columnName}`,
false,
)}
diff --git
a/web-console/src/views/workbench-view/query-parameters-dialog/__snapshots__/query-parameters-dialog.spec.tsx.snap
b/web-console/src/views/workbench-view/query-parameters-dialog/__snapshots__/query-parameters-dialog.spec.tsx.snap
new file mode 100644
index 0000000000..3ea1e60838
--- /dev/null
+++
b/web-console/src/views/workbench-view/query-parameters-dialog/__snapshots__/query-parameters-dialog.spec.tsx.snap
@@ -0,0 +1,457 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QueryParametersDialog matches snapshot 1`] = `
+<Blueprint4.Dialog
+ canOutsideClickClose={true}
+ className="query-parameters-dialog"
+ isOpen={true}
+ onClose={[Function]}
+ title="Dynamic query parameters"
+>
+ <div
+ className="bp4-dialog-body"
+ >
+ <p>
+ Druid SQL supports dynamic parameters using question mark
+ <Unknown>
+ ?
+ </Unknown>
+ syntax, where parameters are bound positionally to ? placeholders at
execution time.
+ </p>
+ <Blueprint4.FormGroup
+ label="Parameter in position 1"
+ >
+ <Blueprint4.ControlGroup
+ fill={true}
+ >
+ <Blueprint4.Popover2
+ boundary="clippingParents"
+ captureDismiss={false}
+ content={
+ <Blueprint4.Menu>
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="tick"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="VARCHAR"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="TIMESTAMP"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="BIGINT"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="DOUBLE"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="FLOAT"
+ />
+ </Blueprint4.Menu>
+ }
+ defaultIsOpen={false}
+ disabled={false}
+ fill={false}
+ hasBackdrop={false}
+ hoverCloseDelay={300}
+ hoverOpenDelay={150}
+ inheritDarkTheme={true}
+ interactionKind="click"
+ matchTargetWidth={false}
+ minimal={true}
+ openOnTargetFocus={true}
+ position="bottom-left"
+ positioningStrategy="absolute"
+ shouldReturnFocusOnClose={false}
+ targetTagName="span"
+ transitionDuration={300}
+ usePortal={true}
+ >
+ <Blueprint4.Button
+ rightIcon="caret-down"
+ text="VARCHAR"
+ />
+ </Blueprint4.Popover2>
+ <Blueprint4.InputGroup
+ fill={true}
+ onChange={[Function]}
+ placeholder="Parameter value"
+ value="Hello world"
+ />
+ <Blueprint4.Button
+ icon="trash"
+ onClick={[Function]}
+ />
+ </Blueprint4.ControlGroup>
+ </Blueprint4.FormGroup>
+ <Blueprint4.FormGroup
+ label="Parameter in position 2"
+ >
+ <Blueprint4.ControlGroup
+ fill={true}
+ >
+ <Blueprint4.Popover2
+ boundary="clippingParents"
+ captureDismiss={false}
+ content={
+ <Blueprint4.Menu>
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="VARCHAR"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="tick"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="TIMESTAMP"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="BIGINT"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="DOUBLE"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="FLOAT"
+ />
+ </Blueprint4.Menu>
+ }
+ defaultIsOpen={false}
+ disabled={false}
+ fill={false}
+ hasBackdrop={false}
+ hoverCloseDelay={300}
+ hoverOpenDelay={150}
+ inheritDarkTheme={true}
+ interactionKind="click"
+ matchTargetWidth={false}
+ minimal={true}
+ openOnTargetFocus={true}
+ position="bottom-left"
+ positioningStrategy="absolute"
+ shouldReturnFocusOnClose={false}
+ targetTagName="span"
+ transitionDuration={300}
+ usePortal={true}
+ >
+ <Blueprint4.Button
+ rightIcon="caret-down"
+ text="TIMESTAMP"
+ />
+ </Blueprint4.Popover2>
+ <Blueprint4.InputGroup
+ fill={true}
+ onChange={[Function]}
+ placeholder="2022-01-01 00:00:00"
+ value="2022-02-02 01:02:03"
+ />
+ <Blueprint4.Button
+ icon="trash"
+ onClick={[Function]}
+ />
+ </Blueprint4.ControlGroup>
+ </Blueprint4.FormGroup>
+ <Blueprint4.FormGroup
+ label="Parameter in position 3"
+ >
+ <Blueprint4.ControlGroup
+ fill={true}
+ >
+ <Blueprint4.Popover2
+ boundary="clippingParents"
+ captureDismiss={false}
+ content={
+ <Blueprint4.Menu>
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="VARCHAR"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="TIMESTAMP"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="tick"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="BIGINT"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="DOUBLE"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="FLOAT"
+ />
+ </Blueprint4.Menu>
+ }
+ defaultIsOpen={false}
+ disabled={false}
+ fill={false}
+ hasBackdrop={false}
+ hoverCloseDelay={300}
+ hoverOpenDelay={150}
+ inheritDarkTheme={true}
+ interactionKind="click"
+ matchTargetWidth={false}
+ minimal={true}
+ openOnTargetFocus={true}
+ position="bottom-left"
+ positioningStrategy="absolute"
+ shouldReturnFocusOnClose={false}
+ targetTagName="span"
+ transitionDuration={300}
+ usePortal={true}
+ >
+ <Blueprint4.Button
+ rightIcon="caret-down"
+ text="BIGINT"
+ />
+ </Blueprint4.Popover2>
+ <Memo(FancyNumericInput)
+ arbitraryPrecision={false}
+ fill={true}
+ onValueChange={[Function]}
+ value={42}
+ />
+ <Blueprint4.Button
+ icon="trash"
+ onClick={[Function]}
+ />
+ </Blueprint4.ControlGroup>
+ </Blueprint4.FormGroup>
+ <Blueprint4.FormGroup
+ label="Parameter in position 4"
+ >
+ <Blueprint4.ControlGroup
+ fill={true}
+ >
+ <Blueprint4.Popover2
+ boundary="clippingParents"
+ captureDismiss={false}
+ content={
+ <Blueprint4.Menu>
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="VARCHAR"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="TIMESTAMP"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="BIGINT"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="tick"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="DOUBLE"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="blank"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="FLOAT"
+ />
+ </Blueprint4.Menu>
+ }
+ defaultIsOpen={false}
+ disabled={false}
+ fill={false}
+ hasBackdrop={false}
+ hoverCloseDelay={300}
+ hoverOpenDelay={150}
+ inheritDarkTheme={true}
+ interactionKind="click"
+ matchTargetWidth={false}
+ minimal={true}
+ openOnTargetFocus={true}
+ position="bottom-left"
+ positioningStrategy="absolute"
+ shouldReturnFocusOnClose={false}
+ targetTagName="span"
+ transitionDuration={300}
+ usePortal={true}
+ >
+ <Blueprint4.Button
+ rightIcon="caret-down"
+ text="DOUBLE"
+ />
+ </Blueprint4.Popover2>
+ <Memo(FancyNumericInput)
+ arbitraryPrecision={true}
+ fill={true}
+ onValueChange={[Function]}
+ value={1.618}
+ />
+ <Blueprint4.Button
+ icon="trash"
+ onClick={[Function]}
+ />
+ </Blueprint4.ControlGroup>
+ </Blueprint4.FormGroup>
+ <Blueprint4.Button
+ icon="plus"
+ onClick={[Function]}
+ text="Add parameter"
+ />
+ </div>
+ <div
+ className="bp4-dialog-footer"
+ >
+ <div
+ className="bp4-dialog-footer-actions"
+ >
+ <Blueprint4.Button
+ onClick={[Function]}
+ text="Close"
+ />
+ <Blueprint4.Button
+ intent="primary"
+ onClick={[Function]}
+ text="Save"
+ />
+ </div>
+ </div>
+</Blueprint4.Dialog>
+`;
diff --git a/web-console/src/components/table-cell/table-cell.scss
b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.scss
similarity index 59%
copy from web-console/src/components/table-cell/table-cell.scss
copy to
web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.scss
index 62c80d4663..caa7f5445c 100644
--- a/web-console/src/components/table-cell/table-cell.scss
+++
b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.scss
@@ -16,39 +16,13 @@
* limitations under the License.
*/
-@import '../../variables';
+@import '../../../variables';
-.table-cell {
- padding: $table-cell-v-padding $table-cell-h-padding;
-
- &.null,
- &.empty {
- font-style: italic;
- }
-
- &.timestamp {
- font-weight: bold;
- }
-
- &.truncated {
+.query-parameters-dialog {
+ .#{$bp-ns}-dialog-body {
position: relative;
- width: 100%;
- display: inline-block;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- padding-right: 16px;
-
- .omitted {
- margin: 0 0.2em;
- font-style: italic;
- }
-
- .action-icon {
- position: absolute;
- top: $table-cell-v-padding;
- right: $table-cell-h-padding;
- color: #f5f8fa;
- }
+ min-height: 50vh;
+ overflow: auto;
+ max-height: 80vh;
}
}
diff --git a/web-console/src/components/table-cell/table-cell.scss
b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.spec.tsx
similarity index 55%
copy from web-console/src/components/table-cell/table-cell.scss
copy to
web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.spec.tsx
index 62c80d4663..539b399cdd 100644
--- a/web-console/src/components/table-cell/table-cell.scss
+++
b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.spec.tsx
@@ -16,39 +16,27 @@
* limitations under the License.
*/
-@import '../../variables';
+import React from 'react';
-.table-cell {
- padding: $table-cell-v-padding $table-cell-h-padding;
+import { shallow } from '../../../utils/shallow-renderer';
- &.null,
- &.empty {
- font-style: italic;
- }
+import { QueryParametersDialog } from './query-parameters-dialog';
- &.timestamp {
- font-weight: bold;
- }
+describe('QueryParametersDialog', () => {
+ it('matches snapshot', () => {
+ const comp = shallow(
+ <QueryParametersDialog
+ queryParameters={[
+ { type: 'VARCHAR', value: 'Hello world' },
+ { type: 'TIMESTAMP', value: '2022-02-02 01:02:03' },
+ { type: 'BIGINT', value: 42 },
+ { type: 'DOUBLE', value: 1.618 },
+ ]}
+ onQueryParametersChange={() => {}}
+ onClose={() => {}}
+ />,
+ );
- &.truncated {
- position: relative;
- width: 100%;
- display: inline-block;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- padding-right: 16px;
-
- .omitted {
- margin: 0 0.2em;
- font-style: italic;
- }
-
- .action-icon {
- position: absolute;
- top: $table-cell-v-padding;
- right: $table-cell-h-padding;
- color: #f5f8fa;
- }
- }
-}
+ expect(comp).toMatchSnapshot();
+ });
+});
diff --git
a/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.tsx
b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.tsx
new file mode 100644
index 0000000000..6a6efda2f2
--- /dev/null
+++
b/web-console/src/views/workbench-view/query-parameters-dialog/query-parameters-dialog.tsx
@@ -0,0 +1,152 @@
+/*
+ * 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 {
+ Button,
+ Classes,
+ Code,
+ ControlGroup,
+ Dialog,
+ FormGroup,
+ InputGroup,
+ Intent,
+ Menu,
+ MenuItem,
+ Position,
+} from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import { Popover2 } from '@blueprintjs/popover2';
+import type { QueryParameter } from '@druid-toolkit/query';
+import { isEmptyArray } from '@druid-toolkit/query';
+import React, { useState } from 'react';
+
+import { FancyNumericInput } from
'../../../components/fancy-numeric-input/fancy-numeric-input';
+import { deepSet, oneOf, tickIcon, without } from '../../../utils';
+
+import './query-parameters-dialog.scss';
+
+const TYPES = ['VARCHAR', 'TIMESTAMP', 'BIGINT', 'DOUBLE', 'FLOAT'];
+
+interface QueryParametersDialogProps {
+ queryParameters: QueryParameter[] | undefined;
+ onQueryParametersChange(parameters: QueryParameter[] | undefined): void;
+ onClose(): void;
+}
+
+export const QueryParametersDialog = React.memo(function QueryParametersDialog(
+ props: QueryParametersDialogProps,
+) {
+ const { queryParameters, onQueryParametersChange, onClose } = props;
+ const [currentQueryParameters, setCurrentQueryParameters] =
useState(queryParameters || []);
+
+ function onSave() {
+ onQueryParametersChange(
+ isEmptyArray(currentQueryParameters) ? undefined :
currentQueryParameters,
+ );
+ onClose();
+ }
+
+ return (
+ <Dialog
+ className="query-parameters-dialog"
+ isOpen
+ onClose={onClose}
+ title="Dynamic query parameters"
+ >
+ <div className={Classes.DIALOG_BODY}>
+ <p>
+ Druid SQL supports dynamic parameters using question mark
<Code>?</Code> syntax, where
+ parameters are bound positionally to ? placeholders at execution
time.
+ </p>
+ {currentQueryParameters.map((queryParameter, i) => {
+ const { type, value } = queryParameter;
+
+ function onValueChange(v: string | number) {
+ setCurrentQueryParameters(deepSet(currentQueryParameters,
`${i}.value`, v));
+ }
+
+ return (
+ <FormGroup key={i} label={`Parameter in position ${i + 1}`}>
+ <ControlGroup fill>
+ <Popover2
+ minimal
+ position={Position.BOTTOM_LEFT}
+ content={
+ <Menu>
+ {TYPES.map(t => (
+ <MenuItem
+ key={t}
+ icon={tickIcon(t === type)}
+ text={t}
+ onClick={() => {
+ setCurrentQueryParameters(
+ deepSet(currentQueryParameters, `${i}.type`, t),
+ );
+ }}
+ />
+ ))}
+ </Menu>
+ }
+ >
+ <Button text={type} rightIcon={IconNames.CARET_DOWN} />
+ </Popover2>
+ {oneOf(type, 'BIGINT', 'DOUBLE', 'FLOAT') ? (
+ <FancyNumericInput
+ value={Number(value)}
+ onValueChange={onValueChange}
+ fill
+ arbitraryPrecision={type !== 'BIGINT'}
+ />
+ ) : (
+ <InputGroup
+ value={String(value)}
+ onChange={(e: any) => onValueChange(e.target.value)}
+ placeholder={type === 'TIMESTAMP' ? '2022-01-01 00:00:00'
: 'Parameter value'}
+ fill
+ />
+ )}
+ <Button
+ icon={IconNames.TRASH}
+ onClick={() => {
+ setCurrentQueryParameters(without(currentQueryParameters,
queryParameter));
+ }}
+ />
+ </ControlGroup>
+ </FormGroup>
+ );
+ })}
+ <Button
+ icon={IconNames.PLUS}
+ text="Add parameter"
+ intent={currentQueryParameters.length ? undefined : Intent.PRIMARY}
+ onClick={() => {
+ setCurrentQueryParameters(
+ currentQueryParameters.concat({ type: 'VARCHAR', value: '' }),
+ );
+ }}
+ />
+ </div>
+ <div className={Classes.DIALOG_FOOTER}>
+ <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+ <Button text="Close" onClick={onClose} />
+ <Button text="Save" intent={Intent.PRIMARY} onClick={onSave} />
+ </div>
+ </div>
+ </Dialog>
+ );
+});
diff --git a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
index d9b5a1a34c..ec1b95ad38 100644
--- a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
+++ b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
@@ -57,6 +57,7 @@ import {
} from '../../../druid-models';
import { deepGet, deepSet, pluralIfNeeded, tickIcon } from '../../../utils';
import { MaxTasksButton } from '../max-tasks-button/max-tasks-button';
+import { QueryParametersDialog } from
'../query-parameters-dialog/query-parameters-dialog';
import './run-panel.scss';
@@ -97,6 +98,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
const { query, onQueryChange, onRun, moreMenu, running, small, queryEngines,
clusterCapacity } =
props;
const [editContextDialogOpen, setEditContextDialogOpen] = useState(false);
+ const [editParametersDialogOpen, setEditParametersDialogOpen] =
useState(false);
const [customTimezoneDialogOpen, setCustomTimezoneDialogOpen] =
useState(false);
const [indexSpecDialogSpec, setIndexSpecDialogSpec] = useState<IndexSpec |
undefined>();
@@ -104,6 +106,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
const ingestMode = query.isIngestQuery();
const queryContext = query.queryContext;
const numContextKeys = Object.keys(queryContext).length;
+ const queryParameters = query.queryParameters;
const maxParseExceptions = getMaxParseExceptions(queryContext);
const finalizeAggregations = getFinalizeAggregations(queryContext);
@@ -238,6 +241,12 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
onClick={() => setEditContextDialogOpen(true)}
label={pluralIfNeeded(numContextKeys, 'key')}
/>
+ <MenuItem
+ icon={IconNames.HELP}
+ text="Define parameters"
+ onClick={() => setEditParametersDialogOpen(true)}
+ label={queryParameters ?
pluralIfNeeded(queryParameters.length, 'parameter') : ''}
+ />
{effectiveEngine !== 'native' && (
<MenuItem
icon={IconNames.GLOBE_NETWORK}
@@ -455,6 +464,15 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
}}
/>
)}
+ {editParametersDialogOpen && (
+ <QueryParametersDialog
+ queryParameters={queryParameters}
+ onQueryParametersChange={p =>
onQueryChange(query.changeQueryParameters(p))}
+ onClose={() => {
+ setEditParametersDialogOpen(false);
+ }}
+ />
+ )}
{customTimezoneDialogOpen && (
<StringInputDialog
title="Custom timezone"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]