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 3e62978a964 Web console: responding to user feedback about the explore
view and fixing bugs (#17844)
3e62978a964 is described below
commit 3e62978a964ae956507932b0684e2ebef7bc6ef4
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Sat Mar 29 07:33:02 2025 -0700
Web console: responding to user feedback about the explore view and fixing
bugs (#17844)
* better debounce
* better cumpose filter
* hook up preview filters
* better stack handling
* fix some props
* refactor stack to facet
* fix hover part 1
* line hover part 2
* start adding moduleWhere
* info popover
* add filter icon
* toggle button
* module filter bar
---
web-console/src/utils/general.tsx | 17 +++
.../src/utils/query-manager/query-manager.ts | 13 +-
.../column-picker-menu/column-picker-menu.tsx | 37 ++++--
.../contains-filter-control.tsx | 6 +-
.../filter-pane/filter-menu/filter-menu.tsx | 28 ++++-
.../regexp-filter-control.tsx | 11 +-
.../time-interval-filter-control.tsx | 50 ++++++--
.../values-filter-control.tsx | 59 +++++----
.../components/filter-pane/filter-pane.tsx | 8 +-
.../src/views/explore-view/components/index.ts | 2 +-
.../iso-date-input.tsx} | 40 ++++--
.../components/module-pane/module-pane.scss | 48 ++++++--
.../components/module-pane/module-pane.tsx | 96 +++++++++------
.../components/preview-pane/preview-pane.scss | 6 +
.../components/preview-pane/preview-pane.tsx | 19 ++-
.../resource-pane/column-dialog/column-dialog.tsx | 26 ++--
.../measure-dialog/measure-dialog.tsx | 26 ++--
.../components/resource-pane/resource-pane.tsx | 6 +-
.../src/views/explore-view/explore-view.scss | 1 +
.../src/views/explore-view/explore-view.tsx | 22 ++--
.../src/views/explore-view/models/module-state.ts | 27 +++--
.../src/views/explore-view/models/parameter.ts | 7 +-
.../src/views/explore-view/models/query-source.ts | 2 +-
.../module-repository/module-repository.ts | 1 +
.../modules/bar-chart-module/bar-chart-module.tsx | 25 +++-
.../grouping-table-module.tsx | 15 +--
.../multi-axis-chart-module.tsx | 15 ++-
.../modules/pie-chart-module/pie-chart-module.tsx | 30 +++--
.../record-table-module/record-table-module.tsx | 15 ++-
.../time-chart-module/continuous-chart-render.scss | 20 +--
.../time-chart-module/continuous-chart-render.tsx | 134 +++++++++++----------
.../time-chart-module/time-chart-module.tsx | 118 +++++++++++-------
32 files changed, 631 insertions(+), 299 deletions(-)
diff --git a/web-console/src/utils/general.tsx
b/web-console/src/utils/general.tsx
index 8af818664fd..d204c7a89e0 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -458,6 +458,23 @@ export function findMap<T, Q>(
return filterMap(xs, f)[0];
}
+export function minBy<T>(xs: T[], f: (item: T, index: number) => number): T |
undefined {
+ if (!xs.length) return undefined;
+
+ let minItem = xs[0];
+ let minValue = f(xs[0], 0);
+
+ for (let i = 1; i < xs.length; i++) {
+ const currentValue = f(xs[i], i);
+ if (currentValue < minValue) {
+ minValue = currentValue;
+ minItem = xs[i];
+ }
+ }
+
+ return minItem;
+}
+
export function changeByIndex<T>(
xs: readonly T[],
i: number,
diff --git a/web-console/src/utils/query-manager/query-manager.ts
b/web-console/src/utils/query-manager/query-manager.ts
index 0b7fcf5680e..b7cbf3ab1c5 100644
--- a/web-console/src/utils/query-manager/query-manager.ts
+++ b/web-console/src/utils/query-manager/query-manager.ts
@@ -39,6 +39,7 @@ export interface QueryManagerOptions<Q, R, I = never, E
extends Error = Error> {
cancelToken: CancelToken,
) => Promise<R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>>;
onStateChange?: (queryResolve: QueryState<R, E, I>) => void;
+ debounceInit?: number;
debounceIdle?: number;
debounceLoading?: number;
backgroundStatusCheckInitDelay?: number;
@@ -78,6 +79,7 @@ export class QueryManager<Q, R, I = never, E extends Error =
Error> {
private state: QueryState<R, E, I>;
private currentQueryId = 0;
+ private readonly runWhenInit: () => void | Promise<void>;
private readonly runWhenIdle: () => void | Promise<void>;
private readonly runWhenLoading: () => void | Promise<void>;
@@ -88,6 +90,11 @@ export class QueryManager<Q, R, I = never, E extends Error =
Error> {
this.backgroundStatusCheckInitDelay =
options.backgroundStatusCheckInitDelay || 500;
this.backgroundStatusCheckDelay = options.backgroundStatusCheckDelay ||
1000;
this.swallowBackgroundError = options.swallowBackgroundError;
+ if (options.debounceInit !== 0) {
+ this.runWhenInit = debounce(this.run, options.debounceInit || 50);
+ } else {
+ this.runWhenInit = this.run;
+ }
if (options.debounceIdle !== 0) {
this.runWhenIdle = debounce(this.run, options.debounceIdle || 100);
} else {
@@ -257,7 +264,11 @@ export class QueryManager<Q, R, I = never, E extends Error
= Error> {
}),
);
- void this.runWhenIdle();
+ if (this.lastQuery) {
+ void this.runWhenIdle();
+ } else {
+ void this.runWhenInit();
+ }
}
}
diff --git
a/web-console/src/views/explore-view/components/column-picker-menu/column-picker-menu.tsx
b/web-console/src/views/explore-view/components/column-picker-menu/column-picker-menu.tsx
index 7634cbae4cc..597df5c4d81 100644
---
a/web-console/src/views/explore-view/components/column-picker-menu/column-picker-menu.tsx
+++
b/web-console/src/views/explore-view/components/column-picker-menu/column-picker-menu.tsx
@@ -17,13 +17,14 @@
*/
import type { IconName } from '@blueprintjs/core';
-import { Icon, InputGroup, Menu, MenuItem } from '@blueprintjs/core';
+import { ContextMenu, Icon, InputGroup, Menu, MenuItem } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import type { Column } from 'druid-query-toolkit';
+import { C } from 'druid-query-toolkit';
import { useState } from 'react';
-import { caseInsensitiveContains, columnToIcon, filterMap } from
'../../../../utils';
+import { caseInsensitiveContains, columnToIcon, copyAndAlert, filterMap } from
'../../../../utils';
import './column-picker-menu.scss';
@@ -66,17 +67,33 @@ export const ColumnPickerMenu = function
ColumnPickerMenu(props: ColumnPickerMen
/>
)}
{filterMap(columns, (c, i) => {
- if (!caseInsensitiveContains(c.name, columnSearch)) return;
+ const columnName = c.name;
+ if (!caseInsensitiveContains(columnName, columnSearch)) return;
const iconName = rightIconForColumn?.(c);
return (
- <MenuItem
+ <ContextMenu
key={i}
- icon={columnToIcon(c) || IconNames.BLANK}
- text={c.name}
- labelElement={iconName && <Icon icon={iconName} />}
- onClick={() => onSelectColumn(c)}
- shouldDismissPopover={shouldDismissPopover}
- />
+ content={
+ <Menu>
+ <MenuItem
+ text="Copy"
+ onClick={() => copyAndAlert(String(columnName), `Copied to
clipboard`)}
+ />
+ <MenuItem
+ text="Copy as SQL column"
+ onClick={() => copyAndAlert(String(C(columnName)), `Copied
to clipboard`)}
+ />
+ </Menu>
+ }
+ >
+ <MenuItem
+ icon={columnToIcon(c) || IconNames.BLANK}
+ text={columnName}
+ labelElement={iconName && <Icon icon={iconName} />}
+ onClick={() => onSelectColumn(c)}
+ shouldDismissPopover={shouldDismissPopover}
+ />
+ </ContextMenu>
);
})}
</Menu>
diff --git
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx
index b3b6892bb4b..15b9a75cadc 100644
---
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx
+++
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx
@@ -30,6 +30,7 @@ import './contains-filter-control.scss';
export interface ContainsFilterControlProps {
querySource: QuerySource;
+ extraFilter: SqlExpression;
filter: SqlExpression;
filterPattern: ContainsFilterPattern;
setFilterPattern(filterPattern: ContainsFilterPattern): void;
@@ -39,7 +40,7 @@ export interface ContainsFilterControlProps {
export const ContainsFilterControl = React.memo(function ContainsFilterControl(
props: ContainsFilterControlProps,
) {
- const { querySource, filter, filterPattern, setFilterPattern, runSqlQuery }
= props;
+ const { querySource, extraFilter, filter, filterPattern, setFilterPattern,
runSqlQuery } = props;
const { column, negated, contains } = filterPattern;
const previewQuery = useMemo(
@@ -47,6 +48,7 @@ export const ContainsFilterControl = React.memo(function
ContainsFilterControl(
querySource
.getInitQuery(
SqlExpression.and(
+ extraFilter,
filter,
contains ? filterPatternToExpression(filterPattern) : undefined,
),
@@ -56,7 +58,7 @@ export const ContainsFilterControl = React.memo(function
ContainsFilterControl(
.changeLimitValue(101)
.toString(),
// eslint-disable-next-line react-hooks/exhaustive-deps -- exclude
'makePattern' from deps
- [querySource.query, filter, column, contains, negated],
+ [querySource.query, extraFilter, filter, column, contains, negated],
);
const [previewState] = useQueryManager<string, string[]>({
diff --git
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/filter-menu.tsx
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/filter-menu.tsx
index b5f5ee2fc9b..39cff40d2a9 100644
---
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/filter-menu.tsx
+++
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/filter-menu.tsx
@@ -108,6 +108,7 @@ type FilterMenuTab = 'compose' | 'sql';
export interface FilterMenuProps {
querySource: QuerySource;
+ extraFilter: SqlExpression;
filter: SqlExpression;
initPattern?: FilterPattern;
onPatternChange(newPattern: FilterPattern): void;
@@ -121,6 +122,7 @@ export interface FilterMenuProps {
export const FilterMenu = React.memo(function FilterMenu(props:
FilterMenuProps) {
const {
querySource,
+ extraFilter,
filter,
initPattern,
onPatternChange,
@@ -136,8 +138,18 @@ export const FilterMenu = React.memo(function
FilterMenu(props: FilterMenuProps)
initPattern?.type === 'custom' ?
filterPatternToExpression(initPattern).toString() : '',
);
const [pattern, setPattern] = useState<FilterPattern |
undefined>(initPattern);
+ const [issue, setIssue] = useState<string | undefined>();
const { columns } = querySource;
+ function setFilterPatternOrIssue(pattern: FilterPattern | undefined, issue:
string | undefined) {
+ if (pattern) {
+ setPattern(pattern);
+ setIssue(undefined);
+ } else {
+ setIssue(issue || 'Issue');
+ }
+ }
+
function onAcceptPattern(pattern: FilterPattern) {
onPatternChange(pattern);
onClose();
@@ -151,6 +163,7 @@ export const FilterMenu = React.memo(function
FilterMenu(props: FilterMenuProps)
cont = (
<ValuesFilterControl
querySource={querySource}
+ extraFilter={extraFilter}
filter={filter}
filterPattern={pattern}
setFilterPattern={setPattern}
@@ -163,6 +176,7 @@ export const FilterMenu = React.memo(function
FilterMenu(props: FilterMenuProps)
cont = (
<ContainsFilterControl
querySource={querySource}
+ extraFilter={extraFilter}
filter={filter}
filterPattern={pattern}
setFilterPattern={setPattern}
@@ -175,6 +189,7 @@ export const FilterMenu = React.memo(function
FilterMenu(props: FilterMenuProps)
cont = (
<RegexpFilterControl
querySource={querySource}
+ extraFilter={extraFilter}
filter={filter}
filterPattern={pattern}
setFilterPattern={setPattern}
@@ -198,7 +213,8 @@ export const FilterMenu = React.memo(function
FilterMenu(props: FilterMenuProps)
<TimeIntervalFilterControl
querySource={querySource}
filterPattern={pattern}
- setFilterPattern={setPattern}
+ setFilterPatternOrIssue={setFilterPatternOrIssue}
+ onIssue={setIssue}
/>
);
break;
@@ -281,6 +297,7 @@ export const FilterMenu = React.memo(function
FilterMenu(props: FilterMenuProps)
active={tab === 'sql'}
onClick={() => {
setFormula(pattern ?
filterPatternToExpression(pattern).toString() : '');
+ setIssue(undefined);
setTab('sql');
}}
/>
@@ -416,8 +433,17 @@ export const FilterMenu = React.memo(function
FilterMenu(props: FilterMenuProps)
intent={Intent.PRIMARY}
text="Apply"
disabled={tab === 'sql' && formula === ''}
+ data-tooltip={issue ? `Issue: ${issue}` : undefined}
onClick={() => {
if (tab === 'compose') {
+ if (issue) {
+ AppToaster.show({
+ message: issue,
+ intent: Intent.DANGER,
+ });
+ return;
+ }
+
if (pattern) {
onAcceptPattern(pattern);
}
diff --git
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx
index 23d67ff2ec5..fa0820667e2 100644
---
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx
+++
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx
@@ -39,6 +39,7 @@ function regexpIssue(possibleRegexp: string): string |
undefined {
export interface RegexpFilterControlProps {
querySource: QuerySource;
+ extraFilter: SqlExpression;
filter: SqlExpression;
filterPattern: RegexpFilterPattern;
setFilterPattern(filterPattern: RegexpFilterPattern): void;
@@ -48,21 +49,25 @@ export interface RegexpFilterControlProps {
export const RegexpFilterControl = React.memo(function RegexpFilterControl(
props: RegexpFilterControlProps,
) {
- const { querySource, filter, filterPattern, setFilterPattern, runSqlQuery }
= props;
+ const { querySource, extraFilter, filter, filterPattern, setFilterPattern,
runSqlQuery } = props;
const { column, negated, regexp } = filterPattern;
const previewQuery = useMemo(
() =>
querySource
.getInitQuery(
- SqlExpression.and(filter, regexp ?
filterPatternToExpression(filterPattern) : undefined),
+ SqlExpression.and(
+ extraFilter,
+ filter,
+ regexp ? filterPatternToExpression(filterPattern) : undefined,
+ ),
)
.addSelect(F.cast(C(column), 'VARCHAR').as('c'), { addToGroupBy: 'end'
})
.changeOrderByExpression(F.count().toOrderByExpression('DESC'))
.changeLimitValue(101)
.toString(),
// eslint-disable-next-line react-hooks/exhaustive-deps -- exclude
'makePattern' from deps
- [querySource.query, filter, column, regexp, negated],
+ [querySource.query, extraFilter, filter, column, regexp, negated],
);
const [previewState] = useQueryManager<string, string[]>({
diff --git
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/time-interval-filter-control/time-interval-filter-control.tsx
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/time-interval-filter-control/time-interval-filter-control.tsx
index 9200ee9e07e..36bee870283 100644
---
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/time-interval-filter-control/time-interval-filter-control.tsx
+++
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/time-interval-filter-control/time-interval-filter-control.tsx
@@ -18,35 +18,69 @@
import { FormGroup } from '@blueprintjs/core';
import type { TimeIntervalFilterPattern } from 'druid-query-toolkit';
-import React from 'react';
+import React, { useState } from 'react';
import type { QuerySource } from '../../../../models';
-import { UtcDateInput } from '../../../utc-date-input/utc-date-input';
+import { IsoDateInput } from '../../../iso-date-input/iso-date-input';
import './time-interval-filter-control.scss';
+function isSwappedFilterPattern(pattern: TimeIntervalFilterPattern) {
+ return pattern.end <= pattern.start;
+}
+
export interface TimeIntervalFilterControlProps {
querySource: QuerySource;
filterPattern: TimeIntervalFilterPattern;
- setFilterPattern(filterPattern: TimeIntervalFilterPattern): void;
+ setFilterPatternOrIssue(
+ filterPattern: TimeIntervalFilterPattern | undefined,
+ issue: string | undefined,
+ ): void;
+ onIssue(issue: string): void;
}
export const TimeIntervalFilterControl = React.memo(function
TimeIntervalFilterControl(
props: TimeIntervalFilterControlProps,
) {
- const { filterPattern, setFilterPattern } = props;
- const { start, end } = filterPattern;
+ const { filterPattern, setFilterPatternOrIssue, onIssue } = props;
+ const [swappedFilterPattern, setSwappedFilterPattern] = useState<
+ TimeIntervalFilterPattern | undefined
+ >();
+ const { start, end } = swappedFilterPattern || filterPattern;
return (
<div className="time-interval-filter-control">
<FormGroup label="Start">
- <UtcDateInput
+ <IsoDateInput
date={start}
- onChange={start => setFilterPattern({ ...filterPattern, start })}
+ onChange={start => {
+ const newPattern = { ...filterPattern, start };
+ if (isSwappedFilterPattern(newPattern)) {
+ setSwappedFilterPattern(newPattern);
+ setFilterPatternOrIssue(undefined, 'Start date must be before
end date');
+ } else {
+ setSwappedFilterPattern(undefined);
+ setFilterPatternOrIssue(newPattern, undefined);
+ }
+ }}
+ onIssue={() => onIssue('Bad start date')}
/>
</FormGroup>
<FormGroup label="End">
- <UtcDateInput date={end} onChange={end => setFilterPattern({
...filterPattern, end })} />
+ <IsoDateInput
+ date={end}
+ onChange={end => {
+ const newPattern = { ...filterPattern, end };
+ if (isSwappedFilterPattern(newPattern)) {
+ setSwappedFilterPattern(newPattern);
+ setFilterPatternOrIssue(undefined, 'End date must be after start
date');
+ } else {
+ setSwappedFilterPattern(undefined);
+ setFilterPatternOrIssue(newPattern, undefined);
+ }
+ }}
+ onIssue={() => onIssue('Bad end date')}
+ />
</FormGroup>
</div>
);
diff --git
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx
index f14851f081f..cf4701fe456 100644
---
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx
+++
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx
@@ -16,16 +16,16 @@
* limitations under the License.
*/
-import { FormGroup, Menu, MenuItem } from '@blueprintjs/core';
+import { ContextMenu, FormGroup, Menu, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { CancelToken } from 'axios';
import type { QueryResult, SqlQuery, ValuesFilterPattern } from
'druid-query-toolkit';
-import { C, F, SqlExpression } from 'druid-query-toolkit';
+import { C, F, L, SqlExpression } from 'druid-query-toolkit';
import React, { useMemo, useState } from 'react';
import { ClearableInput } from '../../../../../../components';
import { useQueryManager } from '../../../../../../hooks';
-import { caseInsensitiveContains, filterMap, toggle } from
'../../../../../../utils';
+import { caseInsensitiveContains, copyAndAlert, filterMap, toggle } from
'../../../../../../utils';
import type { QuerySource } from '../../../../models';
import { ColumnValue } from '../../../column-value/column-value';
@@ -33,6 +33,7 @@ import './values-filter-control.scss';
export interface ValuesFilterControlProps {
querySource: QuerySource;
+ extraFilter: SqlExpression;
filter: SqlExpression;
filterPattern: ValuesFilterPattern;
setFilterPattern(filterPattern: ValuesFilterPattern): void;
@@ -42,7 +43,7 @@ export interface ValuesFilterControlProps {
export const ValuesFilterControl = React.memo(function ValuesFilterControl(
props: ValuesFilterControlProps,
) {
- const { querySource, filter, filterPattern, setFilterPattern, runSqlQuery }
= props;
+ const { querySource, extraFilter, filter, filterPattern, setFilterPattern,
runSqlQuery } = props;
const { column, negated, values: selectedValues } = filterPattern;
const [initValues] = useState(selectedValues);
const [searchString, setSearchString] = useState('');
@@ -52,6 +53,7 @@ export const ValuesFilterControl = React.memo(function
ValuesFilterControl(
querySource
.getInitQuery(
SqlExpression.and(
+ extraFilter,
filter,
searchString ? F('ICONTAINS_STRING', C(column), searchString) :
undefined,
),
@@ -61,7 +63,7 @@ export const ValuesFilterControl = React.memo(function
ValuesFilterControl(
.changeLimitValue(101)
.toString(),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [querySource.query, filter, column, searchString],
+ [querySource.query, extraFilter, filter, column, searchString],
);
const [valuesState] = useQueryManager<string, any[]>({
@@ -90,24 +92,39 @@ export const ValuesFilterControl = React.memo(function
ValuesFilterControl(
{filterMap(valuesToShow, (v, i) => {
if (!caseInsensitiveContains(v, searchString)) return;
return (
- <MenuItem
+ <ContextMenu
key={i}
- icon={
- selectedValues.includes(v)
- ? negated
- ? IconNames.DELETE
- : IconNames.TICK_CIRCLE
- : IconNames.CIRCLE
+ content={
+ <Menu>
+ <MenuItem
+ text="Copy"
+ onClick={() => copyAndAlert(String(v), `Copied to
clipboard`)}
+ />
+ <MenuItem
+ text="Copy as SQL value"
+ onClick={() => copyAndAlert(String(L(v)), `Copied to
clipboard`)}
+ />
+ </Menu>
}
- text={<ColumnValue value={v} />}
- shouldDismissPopover={false}
- onClick={e => {
- setFilterPattern({
- ...filterPattern,
- values: e.altKey ? [v] : toggle(selectedValues, v),
- });
- }}
- />
+ >
+ <MenuItem
+ icon={
+ selectedValues.includes(v)
+ ? negated
+ ? IconNames.DELETE
+ : IconNames.TICK_CIRCLE
+ : IconNames.CIRCLE
+ }
+ text={<ColumnValue value={v} />}
+ shouldDismissPopover={false}
+ onClick={e => {
+ setFilterPattern({
+ ...filterPattern,
+ values: e.altKey ? [v] : toggle(selectedValues, v),
+ });
+ }}
+ />
+ </ContextMenu>
);
})}
{valuesState.loading && <MenuItem icon={IconNames.BLANK}
text="Loading..." disabled />}
diff --git
a/web-console/src/views/explore-view/components/filter-pane/filter-pane.tsx
b/web-console/src/views/explore-view/components/filter-pane/filter-pane.tsx
index cab39295b9f..fa62e0d65cd 100644
--- a/web-console/src/views/explore-view/components/filter-pane/filter-pane.tsx
+++ b/web-console/src/views/explore-view/components/filter-pane/filter-pane.tsx
@@ -48,6 +48,7 @@ import './filter-pane.scss';
export interface FilterPaneProps {
querySource: QuerySource | undefined;
+ extraFilter: SqlExpression;
timezone: Timezone;
filter: SqlExpression;
onFilterChange(filter: SqlExpression): void;
@@ -59,6 +60,7 @@ export interface FilterPaneProps {
export const FilterPane = forwardRef(function FilterPane(props:
FilterPaneProps, ref) {
const {
querySource,
+ extraFilter,
timezone,
filter,
onFilterChange,
@@ -139,6 +141,7 @@ export const FilterPane = forwardRef(function
FilterPane(props: FilterPaneProps,
content={
<FilterMenu
querySource={querySource}
+ extraFilter={extraFilter}
filter={filterPatternsToExpression(without(patterns,
pattern))}
initPattern={pattern}
onPatternChange={newPattern => {
@@ -201,6 +204,7 @@ export const FilterPane = forwardRef(function
FilterPane(props: FilterPaneProps,
content={
<FilterMenu
querySource={querySource}
+ extraFilter={extraFilter}
filter={filter}
initPattern={menuNew?.column ?
initPatternForColumn(menuNew?.column) : undefined}
onPatternChange={newPattern => {
@@ -220,7 +224,7 @@ export const FilterPane = forwardRef(function
FilterPane(props: FilterPaneProps,
text={patterns.length ? undefined : 'Add filter'}
onClick={() => setMenuNew({})}
minimal
- data-tooltip="Add filter"
+ data-tooltip={patterns.length ? 'Add filter' : undefined}
/>
</Popover>
) : (
@@ -229,7 +233,7 @@ export const FilterPane = forwardRef(function
FilterPane(props: FilterPaneProps,
text={patterns.length ? undefined : 'Add filter'}
disabled
minimal
- data-tooltip="Add filter"
+ data-tooltip="No query source, unable to query"
/>
)}
</DroppableContainer>
diff --git a/web-console/src/views/explore-view/components/index.ts
b/web-console/src/views/explore-view/components/index.ts
index 085bbff3c3e..58a8fb6fa72 100644
--- a/web-console/src/views/explore-view/components/index.ts
+++ b/web-console/src/views/explore-view/components/index.ts
@@ -21,6 +21,7 @@ export * from './droppable-container/droppable-container';
export * from './filter-pane/filter-pane';
export * from './generic-output-table/generic-output-table';
export * from './helper-table/helper-table';
+export * from './iso-date-input/iso-date-input';
export * from './issue/issue';
export * from './module-pane/module-pane';
export * from './module-picker/module-picker';
@@ -28,4 +29,3 @@ export * from './resource-pane/resource-pane';
export * from './source-pane/source-pane';
export * from './source-query-pane/source-query-pane';
export * from './sql-input/sql-input';
-export * from './utc-date-input/utc-date-input';
diff --git
a/web-console/src/views/explore-view/components/utc-date-input/utc-date-input.tsx
b/web-console/src/views/explore-view/components/iso-date-input/iso-date-input.tsx
similarity index 62%
rename from
web-console/src/views/explore-view/components/utc-date-input/utc-date-input.tsx
rename to
web-console/src/views/explore-view/components/iso-date-input/iso-date-input.tsx
index b1de4ce7c13..b145e31a221 100644
---
a/web-console/src/views/explore-view/components/utc-date-input/utc-date-input.tsx
+++
b/web-console/src/views/explore-view/components/iso-date-input/iso-date-input.tsx
@@ -16,10 +16,10 @@
* limitations under the License.
*/
-import { InputGroup } from '@blueprintjs/core';
+import { InputGroup, Intent } from '@blueprintjs/core';
import { useState } from 'react';
-function utcParseDate(dateString: string): Date | undefined {
+function isoParseDate(dateString: string): Date | undefined {
const dateParts = dateString.split(/[-T:. ]/g);
// Extract the individual date and time components
@@ -44,7 +44,7 @@ function utcParseDate(dateString: string): Date | undefined {
const millisecond = parseInt(dateParts[6], 10);
if (millisecond >= 1000) return;
- const value = Date.UTC(year, month - 1, day, hour, minute, second); // Month
is zero-based
+ const value = Date.UTC(year, month - 1, day, hour, minute, second,
millisecond); // Month is zero-based
if (isNaN(value)) return;
return new Date(value);
@@ -61,26 +61,42 @@ function formatDate(date: Date): string {
export interface UtcDateInputProps {
date: Date;
onChange(newDate: Date): void;
+ onIssue(): void;
}
-export function UtcDateInput(props: UtcDateInputProps) {
- const { date, onChange } = props;
- const [dateString, setDateString] = useState<string | undefined>();
+export function IsoDateInput(props: UtcDateInputProps) {
+ const { date, onChange, onIssue } = props;
+ const [invalidDateString, setInvalidDateString] = useState<string |
undefined>();
+ const [customDateString, setCustomDateString] = useState<string |
undefined>();
+ const [focused, setFocused] = useState<boolean>(false);
return (
<InputGroup
+ className="iso-date-input"
placeholder="yyyy-MM-dd HH:mm:ss"
- value={dateString ?? formatDate(date)}
+ intent={!focused && invalidDateString ? Intent.DANGER : undefined}
+ value={
+ invalidDateString ??
+ (customDateString && isoParseDate(customDateString)?.valueOf() ===
date.valueOf()
+ ? customDateString
+ : undefined) ??
+ formatDate(date)
+ }
onChange={e => {
- const v = normalizeDateString(e.target.value);
- const parsedDate = utcParseDate(v);
- if (parsedDate && formatDate(parsedDate) === v) {
+ const normalizedDateString = normalizeDateString(e.target.value);
+ const parsedDate = isoParseDate(normalizedDateString);
+ if (parsedDate) {
onChange(parsedDate);
- setDateString(undefined);
+ setInvalidDateString(undefined);
+ setCustomDateString(normalizedDateString);
} else {
- setDateString(v);
+ onIssue();
+ setInvalidDateString(normalizedDateString);
+ setCustomDateString(undefined);
}
}}
+ onFocus={() => setFocused(true)}
+ onBlur={() => setFocused(false)}
/>
);
}
diff --git
a/web-console/src/views/explore-view/components/module-pane/module-pane.scss
b/web-console/src/views/explore-view/components/module-pane/module-pane.scss
index e74e3fe4bae..0fa698cfc9b 100644
--- a/web-console/src/views/explore-view/components/module-pane/module-pane.scss
+++ b/web-console/src/views/explore-view/components/module-pane/module-pane.scss
@@ -18,47 +18,71 @@
@import '../../../../variables';
-$tob-bar-height: 34px;
+$filter-bar-height: 34px;
+$control-bar-height: 34px;
$control-pane-width: 240px;
+$small-border: 1px;
.module-pane {
position: relative;
@include card-like;
overflow: hidden;
- .module-top-bar {
+ .filter-pane {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 90px;
+ height: $filter-bar-height;
+ border-bottom: $small-border solid $dark-gray2;
+ padding: 2px 0;
+ }
+
+ .module-control-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
- height: $tob-bar-height;
- border-bottom: 1px solid $dark-gray2;
+ height: $control-bar-height;
+ border-bottom: $small-border solid $dark-gray2;
padding: 2px;
display: flex;
-
- .bar-expander {
- flex: 1;
- }
}
.control-pane-container {
position: absolute;
- top: $tob-bar-height + 1;
+ top: $control-bar-height + 1;
bottom: 0;
width: $control-pane-width;
right: 0;
- border-left: 1px solid $dark-gray2;
+ border-left: $small-border solid $dark-gray2;
padding: 8px;
overflow: auto;
}
&.show-controls .module-inner-container {
- right: $control-pane-width + 1;
+ right: $control-pane-width + $small-border;
+ }
+
+ &.show-filter {
+ .module-control-bar {
+ top: $filter-bar-height + $small-border;
+ }
+
+ .module-inner-container {
+ top: $filter-bar-height + $small-border + $control-bar-height +
$small-border;
+ }
+ }
+
+ .corner-buttons {
+ position: absolute;
+ top: 2px;
+ right: 2px;
}
.module-inner-container {
position: absolute;
- top: $tob-bar-height + 1;
+ top: $control-bar-height + $small-border;
bottom: 0;
left: 0;
right: 0;
diff --git
a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
index b06e7b1c346..96ed6e8fe90 100644
--- a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
+++ b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
@@ -30,6 +30,7 @@ import { IconNames } from '@blueprintjs/icons';
import type { Timezone } from 'chronoshift';
import classNames from 'classnames';
import type { Column, QueryResult, SqlExpression, SqlQuery } from
'druid-query-toolkit';
+import { SqlLiteral } from 'druid-query-toolkit';
import React, { useState } from 'react';
import { useMemoWithPrevious } from '../../../../hooks';
@@ -53,6 +54,7 @@ import { ModuleRepository } from
'../../module-repository/module-repository';
import { adjustTransferValue, normalizeType } from '../../utils';
import { ControlPane } from '../control-pane/control-pane';
import { DroppableContainer } from
'../droppable-container/droppable-container';
+import { FilterPane } from '../filter-pane/filter-pane';
import { Issue } from '../issue/issue';
import { ModulePicker } from '../module-picker/module-picker';
@@ -115,7 +117,7 @@ export const ModulePane = function ModulePane(props:
ModulePaneProps) {
onAddToSourceQueryAsColumn,
onAddToSourceQueryAsMeasure,
} = props;
- const { moduleId, parameterValues, showControls } = moduleState;
+ const { moduleId, moduleWhere, parameterValues, showModuleWhere,
showControls } = moduleState;
const [stage, setStage] = useState<Stage | undefined>();
const module = ModuleRepository.getModule(moduleId);
@@ -174,6 +176,7 @@ export const ModulePane = function ModulePane(props:
ModulePaneProps) {
timezone,
where,
setWhere,
+ moduleWhere,
parameterValues: parameterValuesWithDefaults,
setParameterValues: updateParameterValues,
runSqlQuery,
@@ -191,15 +194,27 @@ export const ModulePane = function ModulePane(props:
ModulePaneProps) {
setModuleState(moduleState.applyShowMeasure(measure));
}
+ const moduleHasFilter = !SqlLiteral.isTrue(moduleWhere);
return (
<div
className={classNames(
'module-pane',
className,
showControls ? 'show-controls' : 'no-controls',
+ showModuleWhere ? 'show-filter' : 'no-filter',
)}
>
- <div className="module-top-bar">
+ {showModuleWhere && (
+ <FilterPane
+ querySource={querySource}
+ extraFilter={where}
+ timezone={timezone}
+ filter={moduleWhere}
+ onFilterChange={newFilter =>
setModuleState(moduleState.changeModuleWhere(newFilter))}
+ runSqlQuery={runSqlQuery}
+ />
+ )}
+ <div className="module-control-bar">
<ModulePicker
selectedModuleId={moduleId}
onSelectedModuleIdChange={newModuleId => {
@@ -254,42 +269,49 @@ export const ModulePane = function ModulePane(props:
ModulePaneProps) {
onAddToSourceQueryAsMeasure={onAddToSourceQueryAsMeasure}
/>
)}
- <div className="bar-expander" />
- <ButtonGroup>
- <Popover
- position={Position.BOTTOM_RIGHT}
- content={
- <Menu>
- <MenuItem
- icon={IconNames.RESET}
- text="Reset visualization parameters"
- onClick={() => {
- setModuleState(
- moduleState.changeParameterValues(
- getStickyParameterValuesForModule(moduleId),
- ),
- );
- }}
- />
- <MenuItem
- icon={IconNames.TRASH}
- text="Delete module"
- intent={Intent.DANGER}
- onClick={onDelete}
- />
- </Menu>
- }
- >
- <Button icon={IconNames.MORE} data-tooltip="More module options"
minimal />
- </Popover>
- <Button
- icon={IconNames.PROPERTIES}
- data-tooltip={showControls ? 'Hide module controls' : 'Show module
controls'}
- minimal
- onClick={() => setModuleState(moduleState.change({ showControls:
!showControls }))}
- />
- </ButtonGroup>
</div>
+ <ButtonGroup className="corner-buttons">
+ <Popover
+ position={Position.BOTTOM_RIGHT}
+ content={
+ <Menu>
+ <MenuItem
+ icon={IconNames.RESET}
+ text="Reset visualization parameters"
+ onClick={() => {
+ setModuleState(
+
moduleState.changeParameterValues(getStickyParameterValuesForModule(moduleId)),
+ );
+ }}
+ />
+ <MenuItem
+ icon={IconNames.TRASH}
+ text="Delete module"
+ intent={Intent.DANGER}
+ onClick={onDelete}
+ />
+ </Menu>
+ }
+ >
+ <Button icon={IconNames.MORE} data-tooltip="More module options"
minimal />
+ </Popover>
+ <Button
+ icon={moduleHasFilter ? IconNames.FILTER_KEEP : IconNames.FILTER}
+ data-tooltip={`${showModuleWhere ? 'Hide' : 'Show'} module filter
bar${
+ moduleHasFilter ? `\nCurrent filter: ${moduleWhere}` : ''
+ }`}
+ minimal
+ active={showModuleWhere}
+ onClick={() => setModuleState(moduleState.change({ showModuleWhere:
!showModuleWhere }))}
+ />
+ <Button
+ icon={IconNames.PROPERTIES}
+ data-tooltip={`${showControls ? 'Hide' : 'Show'} module controls`}
+ minimal
+ active={showControls}
+ onClick={() => setModuleState(moduleState.change({ showControls:
!showControls }))}
+ />
+ </ButtonGroup>
{showControls && module && (
<div className="control-pane-container">
<ControlPane
diff --git
a/web-console/src/views/explore-view/components/preview-pane/preview-pane.scss
b/web-console/src/views/explore-view/components/preview-pane/preview-pane.scss
index eff7363ad64..8749d7419a1 100644
---
a/web-console/src/views/explore-view/components/preview-pane/preview-pane.scss
+++
b/web-console/src/views/explore-view/components/preview-pane/preview-pane.scss
@@ -21,6 +21,12 @@
display: flex;
flex-direction: column;
+ .info-popover {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ }
+
.preview-values-wrapper {
flex: 1;
position: relative;
diff --git
a/web-console/src/views/explore-view/components/preview-pane/preview-pane.tsx
b/web-console/src/views/explore-view/components/preview-pane/preview-pane.tsx
index 41bfd4bd14a..bb70bda5544 100644
---
a/web-console/src/views/explore-view/components/preview-pane/preview-pane.tsx
+++
b/web-console/src/views/explore-view/components/preview-pane/preview-pane.tsx
@@ -16,11 +16,14 @@
* limitations under the License.
*/
-import { Callout } from '@blueprintjs/core';
+import { Button, Callout, Popover } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import type { QueryResult, SqlQuery } from 'druid-query-toolkit';
+import { dedupe } from 'druid-query-toolkit';
import React from 'react';
+import { PopoverText } from '../../../../components';
import { useQueryManager } from '../../../../hooks';
import { formatEmpty } from '../../../../utils';
@@ -38,27 +41,35 @@ function getPreviewValues(queryResult: QueryResult): any[] {
export interface PreviewPaneProps {
previewQuery: string | undefined;
runSqlQuery(query: string | SqlQuery): Promise<QueryResult>;
+ deduplicate?: boolean;
+ info?: string;
}
export const PreviewPane = React.memo(function PreviewPane(props:
PreviewPaneProps) {
- const { previewQuery, runSqlQuery } = props;
+ const { previewQuery, runSqlQuery, deduplicate, info } = props;
const [previewState] = useQueryManager({
query: previewQuery,
processQuery: runSqlQuery,
- debounceIdle: 1000,
+ debounceIdle: 2000,
+ debounceLoading: 3000,
});
const previewValues = previewState.data ?
getPreviewValues(previewState.data) : undefined;
return (
<Callout className="preview-pane" title="Preview">
+ {info && (
+ <Popover className="info-popover"
content={<PopoverText>{info}</PopoverText>}>
+ <Button icon={IconNames.INFO_SIGN} minimal />
+ </Popover>
+ )}
{previewState.loading && 'Loading...'}
{previewState.error && <div
className="preview-error">{previewState.getErrorMessage()}</div>}
{previewValues &&
(previewValues.length ? (
<div className="preview-values-wrapper">
<div className="preview-values">
- {previewValues.map((v, i) => (
+ {(deduplicate ? dedupe(previewValues) : previewValues).map((v,
i) => (
<div
className={classNames('preview-value', { special: v == null
|| v === '' })}
key={i}
diff --git
a/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx
b/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx
index d220106ec76..2ec95a4b989 100644
---
a/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx
+++
b/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx
@@ -18,7 +18,7 @@
import { Button, Classes, Dialog, FormGroup, InputGroup, Intent, Tag } from
'@blueprintjs/core';
import type { QueryResult, SqlQuery } from 'druid-query-toolkit';
-import { F, sql, SqlExpression } from 'druid-query-toolkit';
+import { F, SqlExpression } from 'druid-query-toolkit';
import React, { useMemo, useState } from 'react';
import { AppToaster } from '../../../../../singletons';
@@ -35,12 +35,14 @@ export interface ColumnDialogProps {
columnToDuplicate?: string;
onApply(newQuery: SqlQuery, rename: Rename | undefined): void;
querySource: QuerySource;
+ where: SqlExpression;
runSqlQuery(query: string | SqlQuery): Promise<QueryResult>;
onClose(): void;
}
export const ColumnDialog = React.memo(function ColumnDialog(props:
ColumnDialogProps) {
- const { initExpression, columnToDuplicate, onApply, querySource,
runSqlQuery, onClose } = props;
+ const { initExpression, columnToDuplicate, onApply, querySource, where,
runSqlQuery, onClose } =
+ props;
const [outputName, setOutputName] = useState(initExpression?.getOutputName()
|| '');
const [formula, setFormula] =
useState(String(initExpression?.getUnderlyingExpression() || ''));
@@ -48,15 +50,16 @@ export const ColumnDialog = React.memo(function
ColumnDialog(props: ColumnDialog
const previewQuery = useMemo(() => {
const expression = SqlExpression.maybeParse(formula);
if (!expression) return;
+
+ const expressionToPreview = F.cast(expression, 'VARCHAR');
return querySource
.getInitBaseQuery()
- .addSelect(F.cast(expression, 'VARCHAR').as('v'), { addToGroupBy: 'end'
})
- .applyIf(querySource.hasBaseTimeColumn(), q =>
- q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`),
- )
- .changeLimitValue(100)
+ .addSelect(expressionToPreview.as('v'))
+ .addWhere(expressionToPreview.isNotNull())
+ .addWhere(querySource.transformExpressionToBaseColumns(where))
+ .changeLimitValue(200)
.toString();
- }, [querySource, formula]);
+ }, [querySource, formula, where]);
return (
<Dialog
@@ -90,7 +93,12 @@ export const ColumnDialog = React.memo(function
ColumnDialog(props: ColumnDialog
/>
</FormGroup>
</div>
- <PreviewPane previewQuery={previewQuery} runSqlQuery={runSqlQuery} />
+ <PreviewPane
+ previewQuery={previewQuery}
+ runSqlQuery={runSqlQuery}
+ deduplicate
+ info="The preview samples values for the column within the selected
filter."
+ />
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
diff --git
a/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx
b/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx
index 308e2563ebf..5299a5dca2e 100644
---
a/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx
+++
b/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx
@@ -17,14 +17,7 @@
*/
import { Button, Classes, Dialog, FormGroup, InputGroup, Intent, Tag } from
'@blueprintjs/core';
-import {
- L,
- type QueryResult,
- sql,
- SqlExpression,
- SqlQuery,
- SqlWithPart,
-} from 'druid-query-toolkit';
+import { L, type QueryResult, SqlExpression, SqlQuery, SqlWithPart } from
'druid-query-toolkit';
import React, { useMemo, useState } from 'react';
import { AppToaster } from '../../../../../singletons';
@@ -40,12 +33,14 @@ export interface MeasureDialogProps {
measureToDuplicate?: string;
onApply(newQuery: SqlQuery, rename: Rename | undefined): void;
querySource: QuerySource;
+ where: SqlExpression;
runSqlQuery(query: string | SqlQuery): Promise<QueryResult>;
onClose(): void;
}
export const MeasureDialog = React.memo(function MeasureDialog(props:
MeasureDialogProps) {
- const { initMeasure, measureToDuplicate, onApply, querySource, runSqlQuery,
onClose } = props;
+ const { initMeasure, measureToDuplicate, onApply, querySource, where,
runSqlQuery, onClose } =
+ props;
const [outputName, setOutputName] = useState(initMeasure?.name || '');
const [formula, setFormula] = useState(String(initMeasure?.expression ||
''));
@@ -57,12 +52,9 @@ export const MeasureDialog = React.memo(function
MeasureDialog(props: MeasureDia
.changeWithParts([SqlWithPart.simple('t',
QuerySource.stripToBaseSource(querySource.query))])
.addSelect(L('Overall').as('label'))
.addSelect(expression.as('value'))
- .applyIf(querySource.hasBaseTimeColumn(), q =>
- q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`),
- )
+
.changeWhereExpression(querySource.transformExpressionToBaseColumns(where))
.toString();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [querySource.query, formula]);
+ }, [querySource, formula, where]);
return (
<Dialog
@@ -96,7 +88,11 @@ export const MeasureDialog = React.memo(function
MeasureDialog(props: MeasureDia
/>
</FormGroup>
</div>
- <PreviewPane previewQuery={previewQuery} runSqlQuery={runSqlQuery} />
+ <PreviewPane
+ previewQuery={previewQuery}
+ runSqlQuery={runSqlQuery}
+ info="The preview shows the overal value for this measure within the
selected filter."
+ />
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
diff --git
a/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx
b/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx
index 9c6146dfd1b..068404a8853 100644
---
a/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx
+++
b/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx
@@ -70,6 +70,7 @@ interface MeasureEditorOpenOn {
export interface ResourcePaneProps {
querySource: QuerySource;
onQueryChange: (newQuery: SqlQuery, rename: Rename | undefined) => void;
+ where: SqlExpression;
onFilter?: (column: Column) => void;
onShowColumn(column: Column): void;
onShowMeasure(measure: Measure): void;
@@ -77,7 +78,8 @@ export interface ResourcePaneProps {
}
export const ResourcePane = function ResourcePane(props: ResourcePaneProps) {
- const { querySource, onQueryChange, onFilter, onShowColumn, onShowMeasure,
runSqlQuery } = props;
+ const { querySource, onQueryChange, where, onFilter, onShowColumn,
onShowMeasure, runSqlQuery } =
+ props;
const [columnSearch, setColumnSearch] = useState('');
const [columnEditorOpenOn, setColumnEditorOpenOn] =
useState<ColumnEditorOpenOn | undefined>();
@@ -317,6 +319,7 @@ export const ResourcePane = function ResourcePane(props:
ResourcePaneProps) {
columnToDuplicate={columnEditorOpenOn.columnToDuplicate}
onApply={onQueryChange}
querySource={querySource}
+ where={where}
runSqlQuery={runSqlQuery}
onClose={() => setColumnEditorOpenOn(undefined)}
/>
@@ -336,6 +339,7 @@ export const ResourcePane = function ResourcePane(props:
ResourcePaneProps) {
measureToDuplicate={measureEditorOpenOn.measureToDuplicate}
onApply={onQueryChange}
querySource={querySource}
+ where={where}
runSqlQuery={runSqlQuery}
onClose={() => setMeasureEditorOpenOn(undefined)}
/>
diff --git a/web-console/src/views/explore-view/explore-view.scss
b/web-console/src/views/explore-view/explore-view.scss
index 54a976f780d..d0541870089 100644
--- a/web-console/src/views/explore-view/explore-view.scss
+++ b/web-console/src/views/explore-view/explore-view.scss
@@ -72,6 +72,7 @@ $resources-width: 240px;
.source-pane-container {
padding: 8px 0;
border-right: 1px solid $dark-gray2;
+ align-self: stretch;
}
.filter-pane {
diff --git a/web-console/src/views/explore-view/explore-view.tsx
b/web-console/src/views/explore-view/explore-view.tsx
index 0d1624cbd7f..fd5b43a51f1 100644
--- a/web-console/src/views/explore-view/explore-view.tsx
+++ b/web-console/src/views/explore-view/explore-view.tsx
@@ -36,7 +36,7 @@ import { Timezone } from 'chronoshift';
import classNames from 'classnames';
import copy from 'copy-to-clipboard';
import type { Column, QueryResult, SqlExpression } from 'druid-query-toolkit';
-import { QueryRunner, SqlQuery } from 'druid-query-toolkit';
+import { QueryRunner, SqlLiteral, SqlQuery } from 'druid-query-toolkit';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Loader, SplitterLayout, TimezoneMenuItems } from '../../components';
@@ -144,9 +144,7 @@ export const ExploreView = React.memo(function
ExploreView() {
'#explore/v/',
LocalStorageKeys.EXPLORE_STATE,
ExploreState.DEFAULT_STATE,
- s => {
- return ExploreState.fromJS(s);
- },
+ s => ExploreState.fromJS(s),
);
// -------------------------------------------------------
@@ -321,6 +319,7 @@ export const ExploreView = React.memo(function
ExploreView() {
<FilterPane
ref={filterPane}
querySource={querySource}
+ extraFilter={SqlLiteral.TRUE}
timezone={timezone}
filter={where}
onFilterChange={setWhere}
@@ -329,7 +328,9 @@ export const ExploreView = React.memo(function
ExploreView() {
if (!querySource) return;
setExploreState(
effectiveExploreState.changeSource(
-
querySource.addColumn(querySource.transformToBaseColumns(expression)),
+ querySource.addColumn(
+
querySource.transformExpressionToBaseColumns(expression),
+ ),
undefined,
),
);
@@ -340,7 +341,9 @@ export const ExploreView = React.memo(function
ExploreView() {
effectiveExploreState
.change({ where: changeWhere })
.changeSource(
-
querySource.addWhereClause(querySource.transformToBaseColumns(expression)),
+ querySource.addWhereClause(
+
querySource.transformExpressionToBaseColumns(expression),
+ ),
undefined,
),
);
@@ -461,6 +464,7 @@ export const ExploreView = React.memo(function
ExploreView() {
<ResourcePane
querySource={querySource}
onQueryChange={setSource}
+ where={where}
onFilter={c => {
filterPane.current?.filterOn(c);
}}
@@ -502,7 +506,7 @@ export const ExploreView = React.memo(function
ExploreView() {
setExploreState(
effectiveExploreState.changeSource(
querySource.addColumn(
-
querySource.transformToBaseColumns(expression),
+
querySource.transformExpressionToBaseColumns(expression),
),
undefined,
),
@@ -514,7 +518,9 @@ export const ExploreView = React.memo(function
ExploreView() {
effectiveExploreState.changeSource(
querySource.addMeasure(
measure.changeExpression(
-
querySource.transformToBaseColumns(measure.expression),
+
querySource.transformExpressionToBaseColumns(
+ measure.expression,
+ ),
),
),
undefined,
diff --git a/web-console/src/views/explore-view/models/module-state.ts
b/web-console/src/views/explore-view/models/module-state.ts
index 93959adea6d..19230fc18af 100644
--- a/web-console/src/views/explore-view/models/module-state.ts
+++ b/web-console/src/views/explore-view/models/module-state.ts
@@ -16,9 +16,9 @@
* limitations under the License.
*/
-import type { Column, SqlExpression } from 'druid-query-toolkit';
+import type { Column } from 'druid-query-toolkit';
+import { SqlExpression, SqlLiteral } from 'druid-query-toolkit';
-import { isEmpty } from '../../../utils';
import { ModuleRepository } from '../module-repository/module-repository';
import type { Rename } from '../utils';
@@ -30,7 +30,9 @@ import type { QuerySource } from './query-source';
interface ModuleStateValue {
moduleId: string;
+ moduleWhere?: SqlExpression;
parameterValues: ParameterValues;
+ showModuleWhere?: boolean;
showControls?: boolean;
}
@@ -44,17 +46,22 @@ export class ModuleState {
);
return new ModuleState({
...js,
+ moduleWhere: SqlExpression.maybeParse(js.moduleWhere),
parameterValues: inflatedParameterValues,
});
}
public readonly moduleId: string;
+ public readonly moduleWhere: SqlExpression;
public readonly parameterValues: ParameterValues;
+ public readonly showModuleWhere: boolean;
public readonly showControls: boolean;
constructor(value: ModuleStateValue) {
this.moduleId = value.moduleId;
+ this.moduleWhere = value.moduleWhere || SqlLiteral.TRUE;
this.parameterValues = value.parameterValues;
+ this.showModuleWhere = Boolean(value.showModuleWhere);
this.showControls = Boolean(value.showControls);
}
@@ -63,6 +70,8 @@ export class ModuleState {
moduleId: this.moduleId,
parameterValues: this.parameterValues,
};
+ if (!SqlLiteral.isTrue(this.moduleWhere)) value.moduleWhere =
this.moduleWhere;
+ if (this.showModuleWhere) value.showModuleWhere = true;
if (this.showControls) value.showControls = true;
return value;
}
@@ -74,6 +83,10 @@ export class ModuleState {
});
}
+ public changeModuleWhere(moduleWhere: SqlExpression): ModuleState {
+ return this.change({ moduleWhere });
+ }
+
public changeParameterValues(parameterValues: ParameterValues): ModuleState {
return this.change({ parameterValues });
}
@@ -92,17 +105,19 @@ export class ModuleState {
}
public restrictToQuerySource(querySource: QuerySource, where:
SqlExpression): ModuleState {
- const { moduleId, parameterValues } = this;
+ const { moduleId, moduleWhere, parameterValues } = this;
const module = ModuleRepository.getModule(moduleId);
if (!module) return this;
+ const newModuleWhere = querySource.restrictWhere(moduleWhere);
const newParameterValues = querySource.restrictParameterValues(
parameterValues,
module.parameters,
where,
);
- if (parameterValues === newParameterValues) return this;
+ if (moduleWhere === newModuleWhere && parameterValues ===
newParameterValues) return this;
return this.change({
+ moduleWhere: newModuleWhere,
parameterValues: newParameterValues,
});
}
@@ -161,10 +176,6 @@ export class ModuleState {
},
});
}
-
- public isInitState(): boolean {
- return this.moduleId === 'record-table' && isEmpty(this.parameterValues);
- }
}
ModuleState.INIT_STATE = new ModuleState({
diff --git a/web-console/src/views/explore-view/models/parameter.ts
b/web-console/src/views/explore-view/models/parameter.ts
index ed021da6f5b..1929795bc19 100644
--- a/web-console/src/views/explore-view/models/parameter.ts
+++ b/web-console/src/views/explore-view/models/parameter.ts
@@ -108,6 +108,7 @@ export type TypedParameterDefinition<Type extends keyof
ParameterTypes> = TypedE
placeholder?: string;
defined?: ModuleFunctor<boolean>;
visible?: ModuleFunctor<boolean>;
+ legacyName?: string;
};
export type ParameterDefinition =
@@ -157,7 +158,11 @@ export function inflateParameterValues(
parameters: Parameters,
): ParameterValues {
return mapRecord(parameters, (parameter, parameterName) =>
- inflateParameterValue(parameterValues?.[parameterName], parameter),
+ inflateParameterValue(
+ parameterValues?.[parameterName] ??
+ (parameter.legacyName ? parameterValues?.[parameter.legacyName] :
undefined),
+ parameter,
+ ),
);
}
diff --git a/web-console/src/views/explore-view/models/query-source.ts
b/web-console/src/views/explore-view/models/query-source.ts
index fa3f047426e..cb67dbc89a9 100644
--- a/web-console/src/views/explore-view/models/query-source.ts
+++ b/web-console/src/views/explore-view/models/query-source.ts
@@ -224,7 +224,7 @@ export class QuerySource {
);
}
- public transformToBaseColumns(expression: SqlExpression): SqlExpression {
+ public transformExpressionToBaseColumns(expression: SqlExpression):
SqlExpression {
const sourceToBaseSubstitutions = this.getSourceToBaseSubstitutions();
return expression.walk(ex => {
if (ex instanceof SqlColumn) {
diff --git
a/web-console/src/views/explore-view/module-repository/module-repository.ts
b/web-console/src/views/explore-view/module-repository/module-repository.ts
index 11bd25145a5..698dcf03672 100644
--- a/web-console/src/views/explore-view/module-repository/module-repository.ts
+++ b/web-console/src/views/explore-view/module-repository/module-repository.ts
@@ -38,6 +38,7 @@ interface ModuleComponentProps<P> {
timezone: Timezone;
where: SqlExpression;
setWhere(where: SqlExpression): void;
+ moduleWhere: SqlExpression;
parameterValues: P;
setParameterValues: (parameters: Partial<P>) => void;
runSqlQuery(
diff --git
a/web-console/src/views/explore-view/modules/bar-chart-module/bar-chart-module.tsx
b/web-console/src/views/explore-view/modules/bar-chart-module/bar-chart-module.tsx
index bdf94e7196e..6d08c77f987 100644
---
a/web-console/src/views/explore-view/modules/bar-chart-module/bar-chart-module.tsx
+++
b/web-console/src/views/explore-view/modules/bar-chart-module/bar-chart-module.tsx
@@ -96,7 +96,16 @@ ModuleRepository.registerModule<BarChartParameterValues>({
},
},
component: function BarChartModule(props) {
- const { querySource, timezone, where, setWhere, parameterValues, stage,
runSqlQuery } = props;
+ const {
+ querySource,
+ timezone,
+ where,
+ setWhere,
+ moduleWhere,
+ parameterValues,
+ stage,
+ runSqlQuery,
+ } = props;
const containerRef = useRef<HTMLDivElement>();
const chartRef = useRef<ECharts>();
const [highlight, setHighlight] = useState<BarChartHighlight |
undefined>();
@@ -108,7 +117,7 @@ ModuleRepository.registerModule<BarChartParameterValues>({
return {
query: querySource
- .getInitQuery(where)
+ .getInitQuery(where.and(moduleWhere))
.addSelect(
splitExpression.applyIf(timeBucket, ex => F.timeFloor(ex,
timeBucket)).as('dim'),
{
@@ -127,7 +136,17 @@ ModuleRepository.registerModule<BarChartParameterValues>({
.changeLimitValue(limit),
timezone,
};
- }, [querySource, timezone, where, splitColumn, timeBucket, measure,
measureToSort, limit]);
+ }, [
+ querySource,
+ timezone,
+ where,
+ moduleWhere,
+ splitColumn,
+ timeBucket,
+ measure,
+ measureToSort,
+ limit,
+ ]);
const [sourceDataState, queryManager] = useQueryManager({
query: dataQuery,
diff --git
a/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
b/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
index eeeee925249..a06b6152722 100644
---
a/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
+++
b/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
@@ -47,7 +47,7 @@ const NEEDS_GROUPING_TO_ORDER = true;
interface QueryAndMore {
timezone: Timezone;
- originalWhere: SqlExpression;
+ globalWhere: SqlExpression;
queryAndHints: QueryAndHints;
}
@@ -213,6 +213,7 @@
ModuleRepository.registerModule<GroupingTableParameterValues>({
timezone,
where,
setWhere,
+ moduleWhere,
parameterValues,
setParameterValues,
runSqlQuery,
@@ -224,13 +225,13 @@
ModuleRepository.registerModule<GroupingTableParameterValues>({
if (!pivotColumn) return;
return querySource
- .getInitQuery(where)
+ .getInitQuery(where.and(moduleWhere))
.addSelect(pivotColumn.expression.as('v'), { addToGroupBy: 'end' })
.changeOrderByExpression(
(measures.length ? measures[0].expression :
F.count()).toOrderByExpression('DESC'),
)
.changeLimitValue(maxPivotValues);
- }, [querySource, where, parameterValues]);
+ }, [querySource, where, moduleWhere, parameterValues]);
const [pivotValueState, queryManager] = useQueryManager({
query: pivotValueQuery,
@@ -249,10 +250,10 @@
ModuleRepository.registerModule<GroupingTableParameterValues>({
return {
timezone,
- originalWhere: where,
+ globalWhere: where,
queryAndHints: makeTableQueryAndHints({
source: querySource.query,
- where,
+ where: where.and(moduleWhere),
splitColumns: parameterValues.splitColumns,
timeBucket: parameterValues.timeBucket,
showColumns: parameterValues.showColumns,
@@ -274,13 +275,13 @@
ModuleRepository.registerModule<GroupingTableParameterValues>({
const [resultState] = useQueryManager({
query: queryAndMore,
processQuery: async (queryAndMore, cancelToken) => {
- const { timezone, originalWhere, queryAndHints } = queryAndMore;
+ const { timezone, globalWhere, queryAndHints } = queryAndMore;
const { query, columnHints } = queryAndHints;
let result = await runSqlQuery({ query, timezone }, cancelToken);
if (result.sqlQuery) {
result = result.attachQuery(
{ query: '' },
- result.sqlQuery.changeWhereExpression(originalWhere),
+ result.sqlQuery.changeWhereExpression(globalWhere),
);
}
return {
diff --git
a/web-console/src/views/explore-view/modules/multi-axis-chart-module/multi-axis-chart-module.tsx
b/web-console/src/views/explore-view/modules/multi-axis-chart-module/multi-axis-chart-module.tsx
index bffb93677aa..2dff7e42eeb 100644
---
a/web-console/src/views/explore-view/modules/multi-axis-chart-module/multi-axis-chart-module.tsx
+++
b/web-console/src/views/explore-view/modules/multi-axis-chart-module/multi-axis-chart-module.tsx
@@ -71,7 +71,16 @@
ModuleRepository.registerModule<MultiAxisChartParameterValues>({
},
},
component: function MultiAxisChartModule(props) {
- const { querySource, timezone, where, setWhere, parameterValues, stage,
runSqlQuery } = props;
+ const {
+ querySource,
+ timezone,
+ where,
+ setWhere,
+ moduleWhere,
+ parameterValues,
+ stage,
+ runSqlQuery,
+ } = props;
const containerRef = useRef<HTMLDivElement>();
const chartRef = useRef<ECharts>();
const [highlight, setHighlight] = useState<MultiAxisChartHighlight |
undefined>();
@@ -87,7 +96,7 @@
ModuleRepository.registerModule<MultiAxisChartParameterValues>({
const dataQuery = useMemo(() => {
return {
query: querySource
- .getInitQuery(where)
+ .getInitQuery(where.and(moduleWhere))
.addSelect(F.timeFloor(C(timeColumnName || '__time'),
L(timeGranularity)).as('time'), {
addToGroupBy: 'end',
addToOrderBy: 'end',
@@ -96,7 +105,7 @@
ModuleRepository.registerModule<MultiAxisChartParameterValues>({
.applyForEach(measures, (q, measure) =>
q.addSelect(measure.expression.as(measure.name))),
timezone,
};
- }, [querySource, timezone, where, timeColumnName, timeGranularity,
measures]);
+ }, [querySource, timezone, where, moduleWhere, timeColumnName,
timeGranularity, measures]);
const [sourceDataState, queryManager] = useQueryManager({
query: dataQuery,
diff --git
a/web-console/src/views/explore-view/modules/pie-chart-module/pie-chart-module.tsx
b/web-console/src/views/explore-view/modules/pie-chart-module/pie-chart-module.tsx
index 69c9b927879..b98c876fb33 100644
---
a/web-console/src/views/explore-view/modules/pie-chart-module/pie-chart-module.tsx
+++
b/web-console/src/views/explore-view/modules/pie-chart-module/pie-chart-module.tsx
@@ -97,7 +97,8 @@ ModuleRepository.registerModule<PieChartParameterValues>({
},
},
component: function PieChartModule(props) {
- const { querySource, where, setWhere, parameterValues, stage, runSqlQuery
} = props;
+ const { querySource, where, setWhere, moduleWhere, parameterValues, stage,
runSqlQuery } =
+ props;
const containerRef = useRef<HTMLDivElement>();
const chartRef = useRef<ECharts>();
const [highlight, setHighlight] = useState<PieChartHighlight |
undefined>();
@@ -106,34 +107,43 @@ ModuleRepository.registerModule<PieChartParameterValues>({
const dataQueries = useMemo(() => {
const splitExpression = splitColumn ? splitColumn.expression :
L(OVERALL_LABEL);
+ const effectiveWhere = where.and(moduleWhere);
return {
mainQuery: querySource
- .getInitQuery(where)
+ .getInitQuery(effectiveWhere)
.addSelect(F.cast(splitExpression, 'VARCHAR').as('name'), {
addToGroupBy: 'end' })
.addSelect(measure.expression.as('value'), {
addToOrderBy: 'end',
direction: 'DESC',
})
- .changeLimitValue(limit),
+ .changeLimitValue(limit + (showOthers ? 1 : 0)),
+ limit,
splitExpression: splitColumn?.expression,
othersPartialQuery: showOthers
- ?
querySource.getInitQuery(where).addSelect(measure.expression.as('value'))
+ ?
querySource.getInitQuery(effectiveWhere).addSelect(measure.expression.as('value'))
: undefined,
};
- }, [querySource, where, splitColumn, measure, limit, showOthers]);
+ }, [querySource, where, moduleWhere, splitColumn, measure, limit,
showOthers]);
const [sourceDataState, queryManager] = useQueryManager({
query: dataQueries,
- processQuery: async ({ mainQuery, splitExpression, othersPartialQuery },
cancelToken) => {
+ processQuery: async (
+ { mainQuery, limit, splitExpression, othersPartialQuery },
+ cancelToken,
+ ) => {
const result = await runSqlQuery({ query: mainQuery }, cancelToken);
const data = result.toObjectArray();
if (splitExpression && othersPartialQuery) {
- const othersResult = await runSqlQuery({
- query:
othersPartialQuery.addWhere(splitExpression.notIn(result.getColumnByIndex(0)!)),
- });
- data.push({ name: 'Others', value: othersResult.rows[0][0],
__isOthers: true });
+ const pieValues = result.getColumnByIndex(0)!;
+
+ if (pieValues.length > limit) {
+ const othersResult = await runSqlQuery({
+ query:
othersPartialQuery.addWhere(splitExpression.notIn(pieValues.slice(0, limit))),
+ });
+ data.push({ name: 'Others', value: othersResult.rows[0][0],
__isOthers: true });
+ }
}
return data;
diff --git
a/web-console/src/views/explore-view/modules/record-table-module/record-table-module.tsx
b/web-console/src/views/explore-view/modules/record-table-module/record-table-module.tsx
index c9d842dfb9c..822f01c329e 100644
---
a/web-console/src/views/explore-view/modules/record-table-module/record-table-module.tsx
+++
b/web-console/src/views/explore-view/modules/record-table-module/record-table-module.tsx
@@ -67,11 +67,20 @@
ModuleRepository.registerModule<RecordTableParameterValues>({
},
},
component: function RecordTableModule(props) {
- const { stage, querySource, timezone, where, setWhere, parameterValues,
runSqlQuery } = props;
+ const {
+ stage,
+ querySource,
+ timezone,
+ where,
+ setWhere,
+ moduleWhere,
+ parameterValues,
+ runSqlQuery,
+ } = props;
const query = useMemo((): string | undefined => {
return SqlQuery.create(querySource.query)
- .changeWhereExpression(where)
+ .changeWhereExpression(where.and(moduleWhere))
.changeLimitValue(parameterValues.maxRows)
.applyIf(
querySource.columns.some(e => e.name === '__time') &&
!parameterValues.ascending,
@@ -79,7 +88,7 @@ ModuleRepository.registerModule<RecordTableParameterValues>({
q => q.changeOrderByClause(querySource.query.orderByClause),
)
.toString();
- }, [querySource, where, parameterValues]);
+ }, [querySource, where, moduleWhere, parameterValues]);
const [resultState, queryManager] = useQueryManager({
query,
diff --git
a/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.scss
b/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.scss
index 5f7be42df95..63f23758091 100644
---
a/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.scss
+++
b/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.scss
@@ -18,6 +18,8 @@
@import '../../../../variables';
+$default-chart-color: $druid-brand;
+
.continuous-chart-render {
position: relative;
overflow: hidden;
@@ -30,10 +32,6 @@
user-select: none;
}
- .mark-bar {
- fill: #497ee6;
- }
-
.selection {
fill: white;
fill-opacity: 0.1;
@@ -89,25 +87,29 @@
opacity: 0.7;
}
+ .mark-bar {
+ fill: #00b6c3;
+ }
+
.mark-line {
stroke-width: 1.5px;
- stroke: #497ee6;
+ stroke: $default-chart-color;
fill: none;
}
.mark-area {
- fill: #497ee6;
- opacity: 0.7;
+ fill: $default-chart-color;
+ opacity: 0.5;
}
.single-point {
- stroke: #497ee6;
+ stroke: $default-chart-color;
opacity: 0.7;
stroke-width: 1.5px;
}
.selected-point {
- fill: #497ee6;
+ fill: $default-chart-color;
}
}
diff --git
a/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.tsx
b/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.tsx
index 378c652142e..c0d759530d5 100644
---
a/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.tsx
+++
b/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.tsx
@@ -42,6 +42,7 @@ import {
formatStartDuration,
groupBy,
lookupBy,
+ minBy,
tickFormatWithTimezone,
timezoneAwareTicks,
} from '../../../../utils';
@@ -73,7 +74,7 @@ export interface RangeDatum {
start: number;
end: number;
measure: number;
- stack: string | undefined;
+ facet: string | undefined;
}
export interface StackedRangeDatum extends RangeDatum {
@@ -126,7 +127,7 @@ export interface ContinuousChartRenderProps {
* If stacking is used then the stack bars should be ordered bottom to top.
*/
data: RangeDatum[];
- stacks: string[] | undefined;
+ facets: string[] | undefined;
/**
* The granularity that was used for bucketing.
@@ -161,7 +162,7 @@ export const ContinuousChartRender = function
ContinuousChartRender(
) {
const {
data,
- stacks,
+ facets,
granularity,
markType,
@@ -199,10 +200,10 @@ export const ContinuousChartRender = function
ContinuousChartRender(
const svgRef = useRef<SVGSVGElement | null>(null);
const stackedData: StackedRangeDatum[] = useMemo(() => {
- const effectiveStacks = stacks || ['undefined'];
- const stackToIndex = lookupBy(
- effectiveStacks,
- s => s,
+ const effectiveFacet = facets || ['undefined'];
+ const facetToIndex = lookupBy(
+ effectiveFacet,
+ f => f,
(_, i) => i,
);
@@ -211,7 +212,7 @@ export const ContinuousChartRender = function
ContinuousChartRender(
const diffStart = b.start - a.start;
if (diffStart) return diffStart;
- return stackToIndex[String(a.stack)] - stackToIndex[String(b.stack)];
+ return facetToIndex[String(a.facet)] - facetToIndex[String(b.facet)];
});
if (markType === 'line') {
@@ -230,18 +231,26 @@ export const ContinuousChartRender = function
ContinuousChartRender(
return withOffset;
});
}
- }, [data, stacks, markType]);
+ }, [data, facets, markType]);
- function findStackedDatum(time: number, measure: number): StackedRangeDatum
| undefined {
+ function findStackedDatum(
+ time: number,
+ measure: number,
+ isStacked: boolean,
+ ): StackedRangeDatum | undefined {
const dataInRange = stackedData.filter(d => d.start <= time && time <
d.end);
if (!dataInRange.length) return;
- return (
- dataInRange.find(r => r.offset <= measure && measure < r.measure +
r.offset) ||
- dataInRange[dataInRange.length - 1]
- );
+ if (isStacked) {
+ return (
+ dataInRange.find(r => r.offset <= measure && measure < r.measure +
r.offset) ||
+ dataInRange[dataInRange.length - 1]
+ );
+ } else {
+ return minBy(dataInRange, r => Math.abs(r.measure - measure));
+ }
}
- const stackColorizer = useMemo(() => {
+ const facetColorizer = useMemo(() => {
const s = scaleOrdinal(COLORS);
return (v: string) => (v === OTHER_VALUE ? OTHER_COLOR : s(v));
}, []);
@@ -342,7 +351,7 @@ export const ContinuousChartRender = function
ContinuousChartRender(
setSelectionIfNeeded({
start: start.valueOf(),
end: end.valueOf(),
- selectedDatum: findStackedDatum(time, measure),
+ selectedDatum: findStackedDatum(time, measure, markType !== 'line'),
});
} else {
setSelection(undefined);
@@ -389,40 +398,43 @@ export const ContinuousChartRender = function
ContinuousChartRender(
}
});
- const byStack = useMemo(() => {
+ const byFacet = useMemo(() => {
if (markType === 'bar' || !stackedData.length) return [];
+ const isStacked = markType !== 'line';
- const effectiveStacks = stacks || ['undefined'];
- const numStacks = effectiveStacks.length;
+ const effectiveFacets = facets || ['undefined'];
+ const numFacets = effectiveFacets.length;
- // Fill in 0s and make sure that the stacks are in the same order
+ // Fill in 0s and make sure that the facets are in the same order
const fullTimeIntervals = groupBy(
stackedData,
d => String(d.start),
dataForStart => {
- if (numStacks === 1) return [dataForStart[0]];
- const stackToDatum = lookupBy(dataForStart, d => d.stack!);
- return effectiveStacks.map(
- (stack, stackIndex) =>
- stackToDatum[stack] || {
+ if (numFacets === 1) return [dataForStart[0]];
+ const facetToDatum = lookupBy(dataForStart, d => d.facet!);
+ return effectiveFacets.map(
+ (facet, facetIndex) =>
+ facetToDatum[facet] || {
...dataForStart[0],
- stack,
+ facet,
measure: 0,
- offset: Math.max(
- 0,
- ...filterMap(effectiveStacks.slice(0, stackIndex), s =>
stackToDatum[s]).map(
- d => d.offset + d.measure,
- ),
- ),
+ offset: isStacked
+ ? Math.max(
+ 0,
+ ...filterMap(effectiveFacets.slice(0, facetIndex), s =>
facetToDatum[s]).map(
+ d => d.offset + d.measure,
+ ),
+ )
+ : 0,
},
);
},
);
// Add nulls to mark gaps in data
- const seriesForStack: Record<string, (StackedRangeDatum | null)[]> = {};
- for (const stack of effectiveStacks) {
- seriesForStack[stack] = [];
+ const seriesForFacet: Record<string, (StackedRangeDatum | null)[]> = {};
+ for (const stack of effectiveFacets) {
+ seriesForFacet[stack] = [];
}
let lastDatum: StackedRangeDatum | undefined;
@@ -430,19 +442,19 @@ export const ContinuousChartRender = function
ContinuousChartRender(
const datum = fullTimeInterval[0];
if (lastDatum && lastDatum.start !== datum.end) {
- for (const stack of effectiveStacks) {
- seriesForStack[stack].push(null);
+ for (const facet of effectiveFacets) {
+ seriesForFacet[facet].push(null);
}
}
- for (let i = 0; i < numStacks; i++) {
- seriesForStack[effectiveStacks[i]].push(fullTimeInterval[i]);
+ for (let i = 0; i < numFacets; i++) {
+ seriesForFacet[effectiveFacets[i]].push(fullTimeInterval[i]);
}
lastDatum = datum;
}
- return Object.values(seriesForStack);
- }, [markType, stackedData, stacks]);
+ return Object.values(seriesForFacet);
+ }, [markType, stackedData, facets]);
if (innerStage.isInvalid()) return;
@@ -531,7 +543,7 @@ export const ContinuousChartRender = function
ContinuousChartRender(
title,
text: (
<>
- {selectedDatum?.stack && <div>{selectedDatum?.stack}</div>}
+ {selectedDatum?.facet && <div>{selectedDatum?.facet}</div>}
<div>{info}</div>
{selection.finalized && (
<div className="button-bar">
@@ -605,13 +617,13 @@ export const ContinuousChartRender = function
ContinuousChartRender(
if (!r) return;
return (
<rect
-
key={`${stackedRow.start}/${stackedRow.end}/${stackedRow.stack}`}
+
key={`${stackedRow.start}/${stackedRow.end}/${stackedRow.facet}`}
className="mark-bar"
{...r}
style={
- typeof stackedRow.stack !== 'undefined'
+ typeof stackedRow.facet !== 'undefined'
? {
- fill: stackColorizer(stackedRow.stack),
+ fill: facetColorizer(stackedRow.facet),
}
: undefined
}
@@ -625,17 +637,17 @@ export const ContinuousChartRender = function
ContinuousChartRender(
/>
)}
{markType === 'area' &&
- byStack.map(ds => {
- const stack = ds[0]!.stack;
+ byFacet.map(ds => {
+ const facet = ds[0]!.facet;
return (
<path
- key={String(stack)}
+ key={String(facet)}
className="mark-area"
d={areaFn(ds)!}
style={
- typeof stack !== 'undefined'
+ typeof facet !== 'undefined'
? {
- fill: stackColorizer(stack),
+ fill: facetColorizer(facet),
}
: undefined
}
@@ -643,17 +655,17 @@ export const ContinuousChartRender = function
ContinuousChartRender(
);
})}
{(markType === 'area' || markType === 'line') &&
- byStack.map(ds => {
- const stack = ds[0]!.stack;
+ byFacet.map(ds => {
+ const facet = ds[0]!.facet;
return (
<path
- key={String(stack)}
+ key={String(facet)}
className="mark-line"
d={lineFn(ds)!}
style={
- typeof stack !== 'undefined'
+ typeof facet !== 'undefined'
? {
- stroke: stackColorizer(stack),
+ stroke: facetColorizer(facet),
}
: undefined
}
@@ -661,22 +673,22 @@ export const ContinuousChartRender = function
ContinuousChartRender(
);
})}
{(markType === 'area' || markType === 'line') &&
- byStack.flatMap(ds =>
+ byFacet.flatMap(ds =>
filterMap(ds, (d, i) => {
if (!d || ds[i - 1] || ds[i + 1]) return; // Not a single
point
const x = timeScale((d.start + d.end) / 2);
return (
<line
- key={`single_${i}_${d.stack}`}
+ key={`single_${i}_${d.facet}`}
className="single-point"
x1={x}
x2={x}
y1={measureScale(d.measure + d.offset)}
y2={measureScale(d.offset)}
style={
- typeof d.stack !== 'undefined'
+ typeof d.facet !== 'undefined'
? {
- stroke: stackColorizer(d.stack),
+ stroke: facetColorizer(d.facet),
}
: undefined
}
@@ -690,9 +702,9 @@ export const ContinuousChartRender = function
ContinuousChartRender(
{...datumToCxCy(selection.selectedDatum)}
r={3}
style={
- typeof selection.selectedDatum.stack !== 'undefined'
+ typeof selection.selectedDatum.facet !== 'undefined'
? {
- fill: stackColorizer(selection.selectedDatum.stack),
+ fill: facetColorizer(selection.selectedDatum.facet),
}
: undefined
}
diff --git
a/web-console/src/views/explore-view/modules/time-chart-module/time-chart-module.tsx
b/web-console/src/views/explore-view/modules/time-chart-module/time-chart-module.tsx
index 5ac00a310d0..cd97284a770 100644
---
a/web-console/src/views/explore-view/modules/time-chart-module/time-chart-module.tsx
+++
b/web-console/src/views/explore-view/modules/time-chart-module/time-chart-module.tsx
@@ -45,7 +45,7 @@ import { ContinuousChartRender, OTHER_VALUE } from
'./continuous-chart-render';
const TIME_NAME = 't';
const MEASURE_NAME = 'm';
-const STACK_NAME = 's';
+const FACET_NAME = 'f';
const MIN_SLICE_WIDTH = 8;
function getRangeInExpression(
@@ -92,12 +92,12 @@ function getRangeInExpression(
}
interface TimeChartParameterValues {
+ markType: ContinuousChartMarkType;
granularity: string;
- splitColumn?: ExpressionMeta;
- numberToStack: number;
+ facetColumn?: ExpressionMeta;
+ maxFacets: number;
showOthers: boolean;
measure: ExpressionMeta;
- markType: ContinuousChartMarkType;
curveType: ContinuousChartCurveType;
}
@@ -106,6 +106,12 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
title: 'Time chart',
icon: IconNames.TIMELINE_LINE_CHART,
parameters: {
+ markType: {
+ type: 'option',
+ options: ['line', 'area', 'bar'],
+ defaultValue: 'line',
+ optionLabels: capitalizeFirst,
+ },
granularity: {
type: 'option',
options: ({ querySource, where }) => {
@@ -132,24 +138,25 @@
ModuleRepository.registerModule<TimeChartParameterValues>({
important: true,
optionLabels: g => (g === 'auto' ? 'Auto' : new
Duration(g).getDescription(true)),
},
- splitColumn: {
+ facetColumn: {
type: 'expression',
- label: 'Stack by',
+ label: 'Facet by',
transferGroup: 'show',
important: true,
+ legacyName: 'splitColumn',
},
- numberToStack: {
+ maxFacets: {
type: 'number',
- label: 'Max stacks',
defaultValue: 7,
- min: 2,
+ min: 1,
required: true,
- visible: ({ parameterValues }) => Boolean(parameterValues.splitColumn),
+ visible: ({ parameterValues }) => Boolean(parameterValues.facetColumn),
+ legacyName: 'numberToStack',
},
showOthers: {
type: 'boolean',
defaultValue: true,
- visible: ({ parameterValues }) => Boolean(parameterValues.splitColumn),
+ visible: ({ parameterValues }) => Boolean(parameterValues.facetColumn),
},
measure: {
type: 'measure',
@@ -159,12 +166,6 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
defaultValue: ({ querySource }) =>
querySource?.getFirstAggregateMeasure(),
required: true,
},
- markType: {
- type: 'option',
- options: ['area', 'bar', 'line'],
- defaultValue: 'area',
- optionLabels: capitalizeFirst,
- },
curveType: {
type: 'option',
options: ['smooth', 'linear', 'step'],
@@ -174,7 +175,16 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
},
},
component: function TimeChartModule(props) {
- const { querySource, timezone, where, setWhere, parameterValues, stage,
runSqlQuery } = props;
+ const {
+ querySource,
+ timezone,
+ where,
+ setWhere,
+ moduleWhere,
+ parameterValues,
+ stage,
+ runSqlQuery,
+ } = props;
const timeColumnName = querySource.columns.find(column => column.sqlType
=== 'TIMESTAMP')?.name;
const timeGranularity =
@@ -186,17 +196,18 @@
ModuleRepository.registerModule<TimeChartParameterValues>({
)
: parameterValues.granularity;
- const { splitColumn, numberToStack, showOthers, measure, markType } =
parameterValues;
+ const { facetColumn, maxFacets, showOthers, measure, markType } =
parameterValues;
const dataQuery = useMemo(() => {
return {
querySource,
timezone,
where,
+ moduleWhere,
timeGranularity,
measure,
- splitExpression: splitColumn?.expression,
- numberToStack,
+ facetExpression: facetColumn?.expression,
+ maxFacets,
showOthers,
oneExtra: markType !== 'bar',
};
@@ -204,10 +215,11 @@
ModuleRepository.registerModule<TimeChartParameterValues>({
querySource,
timezone,
where,
+ moduleWhere,
timeGranularity,
measure,
- splitColumn,
- numberToStack,
+ facetColumn,
+ maxFacets,
showOthers,
markType,
]);
@@ -219,10 +231,11 @@
ModuleRepository.registerModule<TimeChartParameterValues>({
querySource,
timezone,
where,
+ moduleWhere,
timeGranularity,
measure,
- splitExpression,
- numberToStack,
+ facetExpression,
+ maxFacets,
showOthers,
oneExtra,
},
@@ -232,17 +245,20 @@
ModuleRepository.registerModule<TimeChartParameterValues>({
throw new Error(`Must have a column of type TIMESTAMP for the time
chart to work`);
}
+ const effectiveWhere = where.and(moduleWhere);
const granularity = new Duration(timeGranularity);
- const vs = splitExpression
+ const detectedFacets: string[] | undefined = facetExpression
? (
await runSqlQuery(
{
query: querySource
- .getInitQuery(where)
- .addSelect(splitExpression.cast('VARCHAR').as('v'), {
addToGroupBy: 'end' })
+ .getInitQuery(effectiveWhere)
+ .addSelect(facetExpression.cast('VARCHAR').as(FACET_NAME),
{
+ addToGroupBy: 'end',
+ })
.changeOrderByExpression(measure.expression.toOrderByExpression('DESC'))
- .changeLimitValue(numberToStack),
+ .changeLimitValue(maxFacets + (showOthers ? 1 : 0)), // If
we want to show others add 1 to check if we need to query for them
timezone,
},
cancelToken,
@@ -252,44 +268,54 @@
ModuleRepository.registerModule<TimeChartParameterValues>({
cancelToken.throwIfRequested();
- if (vs?.length === 0) {
- // If vs is empty then there is no data at all and no need to do a
larger query
+ if (detectedFacets?.length === 0) {
+ // If detectedFacets is empty then there is no data at all and no
need to do a larger query
return {
- effectiveVs: [],
+ effectiveFacets: [],
sourceData: [],
measure,
granularity,
};
}
- const effectiveVs = vs && showOthers ? vs.concat(OTHER_VALUE) : vs;
+ const facetsToQuery =
+ showOthers && detectedFacets && maxFacets < detectedFacets.length
+ ? detectedFacets.slice(0, maxFacets)
+ : undefined;
+ const effectiveFacets = facetsToQuery ?
facetsToQuery.concat(OTHER_VALUE) : detectedFacets;
const result = await runSqlQuery(
{
query: querySource
- .getInitQuery(overqueryWhere(where, timeColumnName, granularity,
oneExtra))
- .applyIf(splitExpression && vs && !showOthers, q =>
- q.addWhere(splitExpression!.cast('VARCHAR').in(vs!)),
+ .getInitQuery(overqueryWhere(effectiveWhere, timeColumnName,
granularity, oneExtra))
+ .applyIf(facetExpression && detectedFacets && !facetsToQuery, q
=>
+
q.addWhere(facetExpression!.cast('VARCHAR').in(detectedFacets!)),
)
.addSelect(F.timeFloor(C(timeColumnName),
L(timeGranularity)).as(TIME_NAME), {
addToGroupBy: 'end',
addToOrderBy: 'end',
direction: 'DESC',
})
- .applyIf(splitExpression, q => {
- if (!splitExpression || !vs) return q; // Should never get
here, doing this to make peace between eslint and TS
+ .applyIf(facetExpression, q => {
+ if (!facetExpression) return q; // Should never get here,
doing this to make peace between eslint and TS
return q.addSelect(
- (showOthers
- ? SqlCase.ifThenElse(splitExpression.in(vs),
splitExpression, L(OTHER_VALUE))
- : splitExpression
+ (facetsToQuery
+ ? SqlCase.ifThenElse(
+ facetExpression.in(facetsToQuery),
+ facetExpression,
+ L(OTHER_VALUE),
+ )
+ : facetExpression
)
.cast('VARCHAR')
- .as(STACK_NAME),
+ .as(FACET_NAME),
{ addToGroupBy: 'end' },
);
})
.addSelect(measure.expression.as(MEASURE_NAME))
- .changeLimitValue(10000 * (effectiveVs ?
Math.min(effectiveVs.length, 10) : 1)),
+ .changeLimitValue(
+ 10000 * (effectiveFacets ? Math.min(effectiveFacets.length,
10) : 1),
+ ),
timezone,
},
cancelToken,
@@ -300,12 +326,12 @@
ModuleRepository.registerModule<TimeChartParameterValues>({
start: b[TIME_NAME].valueOf(),
end: granularity.shift(b[TIME_NAME], Timezone.UTC, 1).valueOf(),
measure: b[MEASURE_NAME],
- stack: b[STACK_NAME],
+ facet: b[FACET_NAME],
}),
);
return {
- effectiveVs,
+ effectiveFacets,
sourceData: dataset,
measure,
granularity,
@@ -326,7 +352,7 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
{sourceData && (
<ContinuousChartRender
data={sourceData.sourceData}
- stacks={sourceData.effectiveVs}
+ facets={sourceData.effectiveFacets}
granularity={sourceData.granularity}
markType={parameterValues.markType}
curveType={parameterValues.curveType}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]