You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by fj...@apache.org on 2019/08/25 23:56:37 UTC
[incubator-druid] branch master updated: Web console: Save query
context also (#8395)
This is an automated email from the ASF dual-hosted git repository.
fjy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git
The following commit(s) were added to refs/heads/master by this push:
new 9254dc8 Web console: Save query context also (#8395)
9254dc8 is described below
commit 9254dc8b805ec8ac98e61e4e40f68626c91949d0
Author: Vadim Ogievetsky <va...@gmail.com>
AuthorDate: Sun Aug 25 16:56:27 2019 -0700
Web console: Save query context also (#8395)
* tidy up menus
* fix query output API
* save context also
* pull out auto run into a switch
* better copy
* add group_id
* support FLOAT also
* use built in time logic
* fix trunc direction
* add skipCache
* add manifest url
* remove depricated props
---
web-console/console-config.js | 5 +-
web-console/package-lock.json | 6 +-
web-console/package.json | 2 +-
.../src/components/action-cell/action-cell.tsx | 4 -
web-console/src/components/deferred/deferred.tsx | 4 -
.../components/refresh-button/refresh-button.tsx | 5 +-
.../segment-timeline/segment-timeline.tsx | 17 +-
.../src/components/show-value/show-value.tsx | 4 -
.../edit-context-dialog/edit-context-dialog.tsx | 8 +-
web-console/src/dialogs/index.ts | 1 +
.../query-history-dialog/query-history-dialog.tsx | 18 +-
.../__snapshots__/query-plan-dialog.spec.tsx.snap | 14 +-
.../query-plan-dialog/query-plan-dialog.scss | 4 +
.../query-plan-dialog/query-plan-dialog.tsx | 39 +-
.../show-value-dialog/show-value-dialog.tsx | 1 +
web-console/src/utils/local-storage-keys.tsx | 15 +
web-console/src/utils/query-context.tsx | 1 +
web-console/src/utils/sampler.ts | 2 +-
web-console/src/views/index.ts | 1 +
.../src/views/load-data-view/load-data-view.tsx | 3 +-
.../__snapshots__/query-view.spec.tsx.snap | 29 +-
.../__snapshots__/column-tree.spec.tsx.snap | 184 +++++++++-
.../column-tree/column-tree-menu/index.ts} | 4 +-
.../number-menu-items/number-menu-items.spec.tsx | 9 +-
.../number-menu-items/number-menu-items.tsx | 141 ++++---
.../string-menu-items/string-menu-items.spec.tsx | 9 +-
.../string-menu-items/string-menu-items.tsx | 157 ++++----
.../time-menu-items/time-menu-items.spec.tsx | 9 +-
.../time-menu-items/time-menu-items.tsx | 403 ++++++++++-----------
.../query-view/column-tree/column-tree.spec.tsx | 25 +-
.../views/query-view/column-tree/column-tree.tsx | 291 +++++++--------
.../query-view/query-output/query-output.spec.tsx | 9 +-
.../views/query-view/query-output/query-output.tsx | 43 ++-
web-console/src/views/query-view/query-view.scss | 1 +
web-console/src/views/query-view/query-view.tsx | 229 ++++--------
.../query-view/run-button/run-button.spec.tsx | 4 +-
.../src/views/query-view/run-button/run-button.tsx | 32 +-
.../__snapshots__/tasks-view.spec.tsx.snap | 14 +
web-console/src/views/task-view/tasks-view.tsx | 18 +-
web-console/src/visualization/bar-group.tsx | 2 +-
web-console/src/visualization/bar-unit.tsx | 2 +-
web-console/src/visualization/chart-axis.tsx | 2 +-
.../src/visualization/stacked-bar-chart.tsx | 2 +-
43 files changed, 908 insertions(+), 865 deletions(-)
diff --git a/web-console/console-config.js b/web-console/console-config.js
index 127d3a0..6fac8a2 100644
--- a/web-console/console-config.js
+++ b/web-console/console-config.js
@@ -16,4 +16,7 @@
* limitations under the License.
*/
-window.consoleConfig = { /* future configs may go here */ };
+window.consoleConfig = {
+ "exampleManifestsUrl": "https://druid.apache.org/data/example-manifests.tsv"
+ /* future configs may go here */
+};
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 87bb391..a4ae431 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -4428,9 +4428,9 @@
"integrity": "sha512-0sYnfUHHMoajaud/i5BHKA12bUxiWEHJ9rxGqVEppFxsEcxef0TZQ5J59lU+UniEBcz/sG5fTESRyS7cOm3tSQ=="
},
"druid-query-toolkit": {
- "version": "0.3.24",
- "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.24.tgz",
- "integrity": "sha512-kFvEXAjjNuJYpeRsAzzO/cJ2rr4nHBGTSCAA4UPxyt4pKNZE/OUap7IQbsdnxYmhkHgfjUBGcFteufaVHSn7SA==",
+ "version": "0.3.26",
+ "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.26.tgz",
+ "integrity": "sha512-j9HcwHCx2YnFSefYc1oJDw8rPq5zSB0tpGkaMp2GkO9syKbdncKfUPugZ613c5XIOBe+j5Hqh/luqh4sLacHGQ==",
"requires": {
"tslib": "^1.10.0"
}
diff --git a/web-console/package.json b/web-console/package.json
index a221cc8..553938f 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -61,7 +61,7 @@
"d3": "^5.10.1",
"d3-array": "^2.3.1",
"druid-console": "0.0.2",
- "druid-query-toolkit": "^0.3.24",
+ "druid-query-toolkit": "^0.3.26",
"file-saver": "^2.0.2",
"has-own-prop": "^2.0.0",
"hjson": "^3.1.2",
diff --git a/web-console/src/components/action-cell/action-cell.tsx b/web-console/src/components/action-cell/action-cell.tsx
index 248c5ea..f48fc4c 100644
--- a/web-console/src/components/action-cell/action-cell.tsx
+++ b/web-console/src/components/action-cell/action-cell.tsx
@@ -35,10 +35,6 @@ export class ActionCell extends React.PureComponent<ActionCellProps> {
static COLUMN_LABEL = 'Actions';
static COLUMN_WIDTH = 70;
- constructor(props: ActionCellProps, context: any) {
- super(props, context);
- }
-
render(): JSX.Element {
const { onDetail, actions } = this.props;
const actionsMenu = actions ? basicActionsToMenu(actions) : null;
diff --git a/web-console/src/components/deferred/deferred.tsx b/web-console/src/components/deferred/deferred.tsx
index 6194979..7e6cde3 100644
--- a/web-console/src/components/deferred/deferred.tsx
+++ b/web-console/src/components/deferred/deferred.tsx
@@ -25,10 +25,6 @@ export interface DeferredProps {
export interface DeferredState {}
export class Deferred extends React.PureComponent<DeferredProps, DeferredState> {
- constructor(props: DeferredProps, context: any) {
- super(props, context);
- }
-
render(): JSX.Element {
const { content } = this.props;
return content();
diff --git a/web-console/src/components/refresh-button/refresh-button.tsx b/web-console/src/components/refresh-button/refresh-button.tsx
index 9393e50..cfe68e5 100644
--- a/web-console/src/components/refresh-button/refresh-button.tsx
+++ b/web-console/src/components/refresh-button/refresh-button.tsx
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { IconNames } from '@blueprintjs/icons';
import React from 'react';
@@ -27,10 +28,6 @@ export interface RefreshButtonProps {
}
export class RefreshButton extends React.PureComponent<RefreshButtonProps> {
- constructor(props: RefreshButtonProps, context: any) {
- super(props, context);
- }
-
render(): JSX.Element {
const { onRefresh, localStorageKey } = this.props;
const intervals = [
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 34d3a88..dd9eea7 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -27,7 +27,7 @@ import { Loader } from '../loader/loader';
import './segment-timeline.scss';
-interface SegmentTimelineProps extends React.Props<any> {
+interface SegmentTimelineProps {
chartHeight: number;
chartWidth: number;
}
@@ -71,9 +71,7 @@ export interface BarChartMargin {
}
export class SegmentTimeline extends React.Component<SegmentTimelineProps, SegmentTimelineState> {
- private dataQueryManager: QueryManager<null, any>;
- private datasourceQueryManager: QueryManager<null, any>;
- private colors = [
+ static COLORS = [
'#b33040',
'#d25c4d',
'#f2b447',
@@ -91,6 +89,13 @@ export class SegmentTimeline extends React.Component<SegmentTimelineProps, Segme
'#915412',
'#87606c',
];
+
+ static getColor(index: number): string {
+ return SegmentTimeline.COLORS[index % SegmentTimeline.COLORS.length];
+ }
+
+ private dataQueryManager: QueryManager<null, any>;
+ private datasourceQueryManager: QueryManager<null, any>;
private chartMargin = { top: 20, right: 10, bottom: 20, left: 10 };
constructor(props: SegmentTimelineProps) {
@@ -249,7 +254,7 @@ export class SegmentTimeline extends React.Component<SegmentTimelineProps, Segme
y: d[datasource] === undefined ? 0 : d[datasource],
y0,
datasource,
- color: this.colors[i],
+ color: SegmentTimeline.getColor(i),
};
y0 += d[datasource] === undefined ? 0 : d[datasource];
return barUnitData;
@@ -279,7 +284,7 @@ export class SegmentTimeline extends React.Component<SegmentTimelineProps, Segme
x: d.day,
y,
datasource,
- color: this.colors[i],
+ color: SegmentTimeline.getColor(i),
};
});
if (!dataResult.every((d: any) => d.y === 0)) {
diff --git a/web-console/src/components/show-value/show-value.tsx b/web-console/src/components/show-value/show-value.tsx
index f30f4c7..d1e6ce4 100644
--- a/web-console/src/components/show-value/show-value.tsx
+++ b/web-console/src/components/show-value/show-value.tsx
@@ -31,10 +31,6 @@ export interface ShowValueProps {
}
export class ShowValue extends React.PureComponent<ShowValueProps> {
- constructor(props: ShowValueProps, context: any) {
- super(props, context);
- }
-
render(): JSX.Element {
const { endpoint, downloadFilename, jsonValue } = this.props;
return (
diff --git a/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx b/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
index 5a6e320..89e6dca 100644
--- a/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
+++ b/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { Button, Callout, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
import Hjson from 'hjson';
import React from 'react';
@@ -42,6 +43,7 @@ export class EditContextDialog extends React.PureComponent<
constructor(props: EditContextDialogProps) {
super(props);
this.state = {
+ queryContext: props.queryContext,
queryContextString: Object.keys(props.queryContext).length
? JSON.stringify(props.queryContext, undefined, 2)
: '{\n\n}',
@@ -72,10 +74,12 @@ export class EditContextDialog extends React.PureComponent<
};
private handleSave = () => {
- const { onQueryContextChange } = this.props;
+ const { onQueryContextChange, onClose } = this.props;
const { queryContext } = this.state;
if (!queryContext) return;
+
onQueryContextChange(queryContext);
+ onClose();
};
render(): JSX.Element {
@@ -94,9 +98,9 @@ export class EditContextDialog extends React.PureComponent<
<div className={'edit-context-dialog-buttons'}>
<Button text={'Close'} onClick={onClose} />
<Button
- disabled={Boolean(error)}
text={'Save'}
intent={Intent.PRIMARY}
+ disabled={Boolean(error)}
onClick={this.handleSave}
/>
</div>
diff --git a/web-console/src/dialogs/index.ts b/web-console/src/dialogs/index.ts
index 27bd358..0972cdc 100644
--- a/web-console/src/dialogs/index.ts
+++ b/web-console/src/dialogs/index.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
export * from './about-dialog/about-dialog';
export * from './async-action-dialog/async-action-dialog';
export * from './compaction-dialog/compaction-dialog';
diff --git a/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx b/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx
index 129ef82..813b3b6 100644
--- a/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx
+++ b/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx
@@ -26,9 +26,10 @@ import './query-history-dialog.scss';
export interface QueryRecord {
version: string;
queryString: string;
+ queryContext?: Record<string, any>;
}
export interface QueryHistoryDialogProps {
- setQueryString: (queryString: string) => void;
+ setQueryString: (queryString: string, queryContext: Record<string, any>) => void;
onClose: () => void;
queryRecords: readonly QueryRecord[];
}
@@ -51,9 +52,14 @@ export class QueryHistoryDialog extends React.PureComponent<
static addQueryToHistory(
queryHistory: readonly QueryRecord[],
queryString: string,
+ queryContext: Record<string, any>,
): readonly QueryRecord[] {
- // Do not add to history if already the same as the last element
- if (queryHistory.length && queryHistory[0].queryString === queryString) {
+ // Do not add to history if already the same as the last element in query and context
+ if (
+ queryHistory.length &&
+ queryHistory[0].queryString === queryString &&
+ JSON.stringify(queryHistory[0].queryContext) === JSON.stringify(queryContext)
+ ) {
return queryHistory;
}
@@ -61,7 +67,8 @@ export class QueryHistoryDialog extends React.PureComponent<
{
version: QueryHistoryDialog.getHistoryVersion(),
queryString,
- },
+ queryContext,
+ } as QueryRecord,
]
.concat(queryHistory)
.slice(0, 10);
@@ -77,8 +84,9 @@ export class QueryHistoryDialog extends React.PureComponent<
private handleSelect = () => {
const { queryRecords, setQueryString, onClose } = this.props;
const { activeTab } = this.state;
+ const queryRecord = queryRecords[activeTab];
- setQueryString(queryRecords[activeTab].queryString);
+ setQueryString(queryRecord.queryString, queryRecord.queryContext || {});
onClose();
};
diff --git a/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap b/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap
index e91d4b8..e5e442e 100644
--- a/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap
@@ -55,7 +55,9 @@ exports[`query plan dialog matches snapshot 1`] = `
<div
class="bp3-dialog-body"
>
- <div>
+ <div
+ class="generic-result"
+ >
test
</div>
</div>
@@ -75,16 +77,6 @@ exports[`query plan dialog matches snapshot 1`] = `
Close
</span>
</button>
- <button
- class="bp3-button bp3-intent-primary"
- type="button"
- >
- <span
- class="bp3-button-text"
- >
- Open
- </span>
- </button>
</div>
</div>
</div>
diff --git a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.scss b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.scss
index 2835b79..e7797a2 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.scss
+++ b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.scss
@@ -36,4 +36,8 @@
height: 25vh !important;
}
}
+
+ .generic-result {
+ overflow: scroll;
+ }
}
diff --git a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.tsx b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.tsx
index 6297e0c..8959cf7 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.tsx
+++ b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.tsx
@@ -38,23 +38,16 @@ export interface QueryPlanDialogProps {
setQueryString: (queryString: string) => void;
}
-export interface QueryPlanDialogState {}
-
-export class QueryPlanDialog extends React.PureComponent<
- QueryPlanDialogProps,
- QueryPlanDialogState
-> {
+export class QueryPlanDialog extends React.PureComponent<QueryPlanDialogProps> {
constructor(props: QueryPlanDialogProps) {
super(props);
- this.state = {};
}
- private queryString: string = '';
-
render(): JSX.Element {
const { explainResult, explainError, onClose, setQueryString } = this.props;
let content: JSX.Element;
+ let queryString: string | undefined;
if (explainError) {
content = <div>{explainError}</div>;
@@ -71,15 +64,11 @@ export class QueryPlanDialog extends React.PureComponent<
);
}
- this.queryString = JSON.stringify(
- (explainResult as BasicQueryExplanation).query[0],
- undefined,
- 2,
- );
+ queryString = JSON.stringify((explainResult as BasicQueryExplanation).query[0], undefined, 2);
content = (
<div className="one-query">
<FormGroup label="Query">
- <TextArea readOnly value={this.queryString} />
+ <TextArea readOnly value={queryString} />
</FormGroup>
{signature}
</div>
@@ -136,7 +125,7 @@ export class QueryPlanDialog extends React.PureComponent<
</div>
);
} else {
- content = <div>{explainResult}</div>;
+ content = <div className="generic-result">{explainResult}</div>;
}
return (
@@ -145,14 +134,16 @@ export class QueryPlanDialog extends React.PureComponent<
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} />
- <Button
- text="Open"
- intent={Intent.PRIMARY}
- onClick={() => {
- setQueryString(this.queryString);
- onClose();
- }}
- />
+ {queryString && (
+ <Button
+ text="Open query"
+ intent={Intent.PRIMARY}
+ onClick={() => {
+ if (queryString) setQueryString(queryString);
+ onClose();
+ }}
+ />
+ )}
</div>
</div>
</Dialog>
diff --git a/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx b/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
index 60f4a2f..4689dd4 100644
--- a/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
+++ b/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { Button, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import copy from 'copy-to-clipboard';
diff --git a/web-console/src/utils/local-storage-keys.tsx b/web-console/src/utils/local-storage-keys.tsx
index 4f116df..b8fc6f1 100644
--- a/web-console/src/utils/local-storage-keys.tsx
+++ b/web-console/src/utils/local-storage-keys.tsx
@@ -26,6 +26,7 @@ export const LocalStorageKeys = {
SERVER_TABLE_COLUMN_SELECTION: 'historical-table-column-selection' as 'historical-table-column-selection',
LOOKUP_TABLE_COLUMN_SELECTION: 'lookup-table-column-selection' as 'lookup-table-column-selection',
QUERY_KEY: 'druid-console-query' as 'druid-console-query',
+ QUERY_CONTEXT: 'query-context' as 'query-context',
TASKS_VIEW_PANE_SIZE: 'tasks-view-pane-size' as 'tasks-view-pane-size',
QUERY_VIEW_PANE_SIZE: 'query-view-pane-size' as 'query-view-pane-size',
TASKS_REFRESH_RATE: 'task-refresh-rate' as 'task-refresh-rate',
@@ -46,7 +47,21 @@ export function localStorageSet(key: LocalStorageKeys, value: string): void {
localStorage.setItem(key, value);
}
+export function localStorageSetJson(key: LocalStorageKeys, value: any): void {
+ localStorageSet(key, JSON.stringify(value));
+}
+
export function localStorageGet(key: LocalStorageKeys): string | undefined {
if (typeof localStorage === 'undefined') return;
return localStorage.getItem(key) || undefined;
}
+
+export function localStorageGetJson(key: LocalStorageKeys): any {
+ const value = localStorageGet(key);
+ if (!value) return;
+ try {
+ return JSON.parse(value);
+ } catch {
+ return;
+ }
+}
diff --git a/web-console/src/utils/query-context.tsx b/web-console/src/utils/query-context.tsx
index d470885..4449c5d 100644
--- a/web-console/src/utils/query-context.tsx
+++ b/web-console/src/utils/query-context.tsx
@@ -23,6 +23,7 @@ export interface QueryContext {
populateCache?: boolean | undefined;
useApproximateCountDistinct?: boolean | undefined;
useApproximateTopN?: boolean | undefined;
+ [key: string]: any;
}
export function isEmptyContext(context: QueryContext): boolean {
diff --git a/web-console/src/utils/sampler.ts b/web-console/src/utils/sampler.ts
index 144dc73..bd33dd2 100644
--- a/web-console/src/utils/sampler.ts
+++ b/web-console/src/utils/sampler.ts
@@ -555,7 +555,7 @@ export async function sampleForExampleManifests(
},
},
},
- samplerConfig: { numRows: 50, timeoutMs: 10000 },
+ samplerConfig: { numRows: 50, timeoutMs: 10000, skipCache: true },
};
const exampleData = await postToSampler(sampleSpec, 'example-manifest');
diff --git a/web-console/src/views/index.ts b/web-console/src/views/index.ts
index 0a4a0bb..bea772f 100644
--- a/web-console/src/views/index.ts
+++ b/web-console/src/views/index.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
export * from './datasource-view/datasource-view';
export * from './home-view/home-view';
export * from './load-data-view/load-data-view';
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx b/web-console/src/views/load-data-view/load-data-view.tsx
index 47be1c6..71d71c8 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -1044,8 +1044,9 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
</FormGroup>
)}
<Button
- text={inlineMode ? 'Register' : 'Preview'}
+ text={inlineMode ? 'Register data' : 'Preview'}
disabled={isBlank}
+ intent={inputQueryState.data ? undefined : Intent.PRIMARY}
onClick={() => this.queryForConnect()}
/>
</div>
diff --git a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
index 21f6ac7..38b279c 100644
--- a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
+++ b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
@@ -5,16 +5,10 @@ exports[`sql view matches snapshot 1`] = `
className="query-view app-view"
>
<ColumnTree
- addAggregateColumn={[Function]}
- addFunctionToGroupBy={[Function]}
- addToGroupBy={[Function]}
- clear={[Function]}
columnMetadataLoading={true}
defaultSchema="druid"
- filterByRow={[Function]}
+ getParsedQuery={[Function]}
onQueryStringChange={[Function]}
- queryAst={[Function]}
- replaceFrom={[Function]}
/>
<t
customClassName=""
@@ -41,8 +35,6 @@ exports[`sql view matches snapshot 1`] = `
className="control-bar"
>
<HotkeysTarget(RunButton)
- autoRun={true}
- onAutoRunChange={[Function]}
onEditContext={[Function]}
onExplain={[Function]}
onHistory={[Function]}
@@ -52,7 +44,20 @@ exports[`sql view matches snapshot 1`] = `
runeMode={false}
/>
<Blueprint3.Tooltip
- content="Automatically wrap the query with a limit to protect against queries with very large result sets"
+ content="Automatically run queries when modified via helper action menus."
+ hoverCloseDelay={0}
+ hoverOpenDelay={800}
+ transitionDuration={100}
+ >
+ <Blueprint3.Switch
+ checked={true}
+ className="auto-run"
+ label="Auto run"
+ onChange={[Function]}
+ />
+ </Blueprint3.Tooltip>
+ <Blueprint3.Tooltip
+ content="Automatically wrap the query with a limit to protect against queries with very large result sets."
hoverCloseDelay={0}
hoverOpenDelay={800}
transitionDuration={100}
@@ -68,10 +73,8 @@ exports[`sql view matches snapshot 1`] = `
</div>
<QueryOutput
loading={false}
+ onQueryChange={[Function]}
runeMode={false}
- sqlExcludeColumn={[Function]}
- sqlFilterRow={[Function]}
- sqlOrderBy={[Function]}
/>
</t>
</div>
diff --git a/web-console/src/views/query-view/column-tree/__snapshots__/column-tree.spec.tsx.snap b/web-console/src/views/query-view/column-tree/__snapshots__/column-tree.spec.tsx.snap
index 2a29f0b..3ce9146 100644
--- a/web-console/src/views/query-view/column-tree/__snapshots__/column-tree.spec.tsx.snap
+++ b/web-console/src/views/query-view/column-tree/__snapshots__/column-tree.spec.tsx.snap
@@ -49,13 +49,13 @@ exports[`column tree matches snapshot 1`] = `
class="bp3-tree-node-list bp3-tree-root"
>
<li
- class="bp3-tree-node"
+ class="bp3-tree-node bp3-tree-node-expanded"
>
<div
class="bp3-tree-node-content bp3-tree-node-content-0"
>
<span
- class="bp3-icon bp3-icon-chevron-right bp3-tree-node-caret bp3-tree-node-caret-closed"
+ class="bp3-icon bp3-icon-chevron-right bp3-tree-node-caret bp3-tree-node-caret-open"
icon="chevron-right"
>
<svg
@@ -104,7 +104,7 @@ exports[`column tree matches snapshot 1`] = `
<div
class=""
>
- deletion-tutorial
+ wikipedia
</div>
</span>
</span>
@@ -112,12 +112,186 @@ exports[`column tree matches snapshot 1`] = `
</div>
<div
class="bp3-collapse"
+ style="height: auto; overflow-y: visible; transition: none;"
>
<div
aria-hidden="false"
class="bp3-collapse-body"
- style="transform: translateY(-0px);"
- />
+ style="transform: translateY(0); transition: none;"
+ >
+ <ul
+ class="bp3-tree-node-list"
+ >
+ <li
+ class="bp3-tree-node"
+ >
+ <div
+ class="bp3-tree-node-content bp3-tree-node-content-1"
+ >
+ <span
+ class="bp3-tree-node-caret-none"
+ />
+ <span
+ class="bp3-icon bp3-icon-time bp3-tree-node-icon"
+ icon="time"
+ >
+ <svg
+ data-icon="time"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ time
+ </desc>
+ <path
+ d="M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm1-6.41V4c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .28.11.53.29.71l2 2a1.003 1.003 0 001.42-1.42L9 7.59z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+ <span
+ class="bp3-tree-node-label"
+ >
+ <span
+ class="bp3-popover-wrapper"
+ >
+ <span
+ class="bp3-popover-target bp3-popover-open"
+ >
+ <div
+ class=""
+ >
+ __time
+ </div>
+ </span>
+ </span>
+ </span>
+ </div>
+ <div
+ class="bp3-collapse"
+ >
+ <div
+ aria-hidden="false"
+ class="bp3-collapse-body"
+ style="transform: translateY(-0px);"
+ />
+ </div>
+ </li>
+ <li
+ class="bp3-tree-node"
+ >
+ <div
+ class="bp3-tree-node-content bp3-tree-node-content-1"
+ >
+ <span
+ class="bp3-tree-node-caret-none"
+ />
+ <span
+ class="bp3-icon bp3-icon-numerical bp3-tree-node-icon"
+ icon="numerical"
+ >
+ <svg
+ data-icon="numerical"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ numerical
+ </desc>
+ <path
+ d="M2.79 4.61c-.13.17-.29.3-.48.41-.18.11-.39.18-.62.23-.23.04-.46.07-.71.07v1.03h1.74V12h1.26V4h-.94c-.04.23-.12.44-.25.61zm4.37 5.31c.18-.14.37-.28.58-.42l.63-.45c.21-.16.41-.33.61-.51s.37-.38.52-.59c.15-.21.28-.45.37-.7.09-.25.13-.54.13-.85 0-.25-.04-.52-.12-.8-.07-.29-.2-.55-.39-.79a2.18 2.18 0 00-.73-.6c-.29-.15-.66-.23-1.11-.23-.41 0-.77.08-1.08.23-.31.16-.58.37-.79.64-.22.27-.38.59-.49.96-.11.37-.16.77-.16 1.2h1.19c.01-.27.03-.53.08-.77.04-.24.11-.45.21-. [...]
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+ <span
+ class="bp3-tree-node-label"
+ >
+ <span
+ class="bp3-popover-wrapper"
+ >
+ <span
+ class="bp3-popover-target bp3-popover-open"
+ >
+ <div
+ class=""
+ >
+ added
+ </div>
+ </span>
+ </span>
+ </span>
+ </div>
+ <div
+ class="bp3-collapse"
+ >
+ <div
+ aria-hidden="false"
+ class="bp3-collapse-body"
+ style="transform: translateY(-0px);"
+ />
+ </div>
+ </li>
+ <li
+ class="bp3-tree-node"
+ >
+ <div
+ class="bp3-tree-node-content bp3-tree-node-content-1"
+ >
+ <span
+ class="bp3-tree-node-caret-none"
+ />
+ <span
+ class="bp3-icon bp3-icon-numerical bp3-tree-node-icon"
+ icon="numerical"
+ >
+ <svg
+ data-icon="numerical"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ numerical
+ </desc>
+ <path
+ d="M2.79 4.61c-.13.17-.29.3-.48.41-.18.11-.39.18-.62.23-.23.04-.46.07-.71.07v1.03h1.74V12h1.26V4h-.94c-.04.23-.12.44-.25.61zm4.37 5.31c.18-.14.37-.28.58-.42l.63-.45c.21-.16.41-.33.61-.51s.37-.38.52-.59c.15-.21.28-.45.37-.7.09-.25.13-.54.13-.85 0-.25-.04-.52-.12-.8-.07-.29-.2-.55-.39-.79a2.18 2.18 0 00-.73-.6c-.29-.15-.66-.23-1.11-.23-.41 0-.77.08-1.08.23-.31.16-.58.37-.79.64-.22.27-.38.59-.49.96-.11.37-.16.77-.16 1.2h1.19c.01-.27.03-.53.08-.77.04-.24.11-.45.21-. [...]
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+ <span
+ class="bp3-tree-node-label"
+ >
+ <span
+ class="bp3-popover-wrapper"
+ >
+ <span
+ class="bp3-popover-target bp3-popover-open"
+ >
+ <div
+ class=""
+ >
+ addedBy10
+ </div>
+ </span>
+ </span>
+ </span>
+ </div>
+ <div
+ class="bp3-collapse"
+ >
+ <div
+ aria-hidden="false"
+ class="bp3-collapse-body"
+ style="transform: translateY(-0px);"
+ />
+ </div>
+ </li>
+ </ul>
+ </div>
</div>
</li>
</ul>
diff --git a/web-console/console-config.js b/web-console/src/views/query-view/column-tree/column-tree-menu/index.ts
similarity index 83%
copy from web-console/console-config.js
copy to web-console/src/views/query-view/column-tree/column-tree-menu/index.ts
index 127d3a0..0af13c5 100644
--- a/web-console/console-config.js
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/index.ts
@@ -16,4 +16,6 @@
* limitations under the License.
*/
-window.consoleConfig = { /* future configs may go here */ };
+export * from './number-menu-items/number-menu-items';
+export * from './string-menu-items/string-menu-items';
+export * from './time-menu-items/time-menu-items';
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
index 0afa060..d41537d 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
@@ -28,14 +28,9 @@ describe('number menu', () => {
it('matches snapshot', () => {
const numberMenu = (
<NumberMenuItems
- hasFilter
- clear={() => null}
- addFunctionToGroupBy={() => null}
- addToGroupBy={() => null}
- addAggregateColumn={() => null}
- filterByRow={() => null}
columnName={'added'}
- queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+ parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+ onQueryChange={() => {}}
/>
);
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
index 3ec2b9f..e08195e 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
@@ -18,139 +18,134 @@
import { MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import { Alias, FilterClause, SqlQuery, StringType } from 'druid-query-toolkit';
+import { SqlQuery, StringType } from 'druid-query-toolkit';
import { aliasFactory } from 'druid-query-toolkit/build/ast/sql-query/helpers';
import React from 'react';
-import { RowFilter } from '../../../query-view';
-
export interface NumberMenuItemsProps {
- addFunctionToGroupBy: (
- functionName: string,
- spacing: string[],
- argumentsArray: (StringType | number)[],
- run: boolean,
- alias: Alias,
- ) => void;
- addToGroupBy: (columnName: string, run: boolean) => void;
- addAggregateColumn: (
- columnName: string,
- functionName: string,
- run: boolean,
- alias?: Alias,
- distinct?: boolean,
- filter?: FilterClause,
- ) => void;
- filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
- queryAst?: SqlQuery;
columnName: string;
- clear: (column: string, preferablyRun: boolean) => void;
- hasFilter: boolean;
+ parsedQuery: SqlQuery;
+ onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
}
export class NumberMenuItems extends React.PureComponent<NumberMenuItemsProps> {
- constructor(props: NumberMenuItemsProps, context: any) {
- super(props, context);
- }
-
renderFilterMenu(): JSX.Element {
- const { columnName, filterByRow } = this.props;
+ const { columnName, parsedQuery, onQueryChange } = this.props;
return (
<MenuItem icon={IconNames.FILTER} text={`Filter`}>
<MenuItem
text={`"${columnName}" > 100`}
- onClick={() => filterByRow([{ row: 100, header: columnName, operator: '>' }], false)}
+ onClick={() => {
+ onQueryChange(parsedQuery.filterRow(columnName, 100, '>'));
+ }}
/>
<MenuItem
text={`"${columnName}" <= 100`}
- onClick={() => filterByRow([{ row: 100, header: columnName, operator: '<=' }], false)}
+ onClick={() => {
+ onQueryChange(parsedQuery.filterRow(columnName, 100, '<='));
+ }}
/>
</MenuItem>
);
}
- renderRemoveFilter() {
- const { columnName, clear } = this.props;
+ renderRemoveFilter(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.hasFilterForColumn(columnName)) return;
+
return (
<MenuItem
icon={IconNames.FILTER_REMOVE}
text={`Remove filter`}
onClick={() => {
- clear(columnName, true);
+ onQueryChange(parsedQuery.removeFilter(columnName), true);
}}
/>
);
}
- renderGroupByMenu(): JSX.Element {
- const { columnName, addFunctionToGroupBy, addToGroupBy } = this.props;
+ renderGroupByMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.groupByClause) return;
return (
<MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
- <MenuItem text={`"${columnName}"`} onClick={() => addToGroupBy(columnName, true)} />
<MenuItem
- text={`TRUNCATE("${columnName}", 1) AS "${columnName}_truncated"`}
- onClick={() =>
- addFunctionToGroupBy(
- 'TRUNCATE',
- [' '],
- [
- new StringType({
- spacing: [],
- chars: columnName,
- quote: '"',
- }),
- 1,
- ],
+ text={`"${columnName}"`}
+ onClick={() => {
+ onQueryChange(parsedQuery.addToGroupBy(columnName), true);
+ }}
+ />
+ <MenuItem
+ text={`TRUNC("${columnName}", -1) AS "${columnName}_trunc"`}
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addFunctionToGroupBy(
+ 'TRUNC',
+ [' '],
+ [
+ new StringType({
+ spacing: [],
+ chars: columnName,
+ quote: '"',
+ }),
+ -1,
+ ],
+ aliasFactory(`${columnName}_truncated`),
+ ),
true,
- aliasFactory(`${columnName}_truncated`),
- )
- }
+ );
+ }}
/>
</MenuItem>
);
}
- renderAggregateMenu(): JSX.Element {
- const { columnName, addAggregateColumn } = this.props;
+ renderAggregateMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.groupByClause) return;
+
return (
<MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
<MenuItem
text={`SUM(${columnName}) AS "sum_${columnName}"`}
- onClick={() =>
- addAggregateColumn(columnName, 'SUM', true, aliasFactory(`sum_${columnName}`))
- }
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addAggregateColumn(columnName, 'SUM', aliasFactory(`sum_${columnName}`)),
+ true,
+ );
+ }}
/>
<MenuItem
text={`MAX(${columnName}) AS "max_${columnName}"`}
- onClick={() =>
- addAggregateColumn(columnName, 'MAX', true, aliasFactory(`max_${columnName}`))
- }
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addAggregateColumn(columnName, 'MAX', aliasFactory(`max_${columnName}`)),
+ true,
+ );
+ }}
/>
<MenuItem
text={`MIN(${columnName}) AS "min_${columnName}"`}
- onClick={() =>
- addAggregateColumn(columnName, 'MIN', true, aliasFactory(`min_${columnName}`))
- }
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addAggregateColumn(columnName, 'MIN', aliasFactory(`min_${columnName}`)),
+ true,
+ );
+ }}
/>
</MenuItem>
);
}
render(): JSX.Element {
- const { queryAst, hasFilter } = this.props;
- let hasGroupBy;
- if (queryAst) {
- hasGroupBy = queryAst.groupByClause;
- }
-
return (
<>
- {queryAst && this.renderFilterMenu()}
- {hasFilter && this.renderRemoveFilter()}
- {hasGroupBy && this.renderGroupByMenu()}
- {hasGroupBy && this.renderAggregateMenu()}
+ {this.renderFilterMenu()}
+ {this.renderRemoveFilter()}
+ {this.renderGroupByMenu()}
+ {this.renderAggregateMenu()}
</>
);
}
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
index 1e4ffb5..950b74e 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
@@ -28,14 +28,9 @@ describe('string menu', () => {
it('matches snapshot', () => {
const stringMenu = (
<StringMenuItems
- hasFilter
- clear={() => null}
- addFunctionToGroupBy={() => null}
- addToGroupBy={() => null}
- addAggregateColumn={() => null}
- filterByRow={() => null}
columnName={'channel'}
- queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+ parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+ onQueryChange={() => {}}
/>
);
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
index 9b508f9..deabcf2 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
@@ -19,160 +19,151 @@
import { MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import {
- Alias,
ComparisonExpression,
ComparisonExpressionRhs,
FilterClause,
- RefExpression,
refExpressionFactory,
SqlQuery,
- StringType,
WhereClause,
} from 'druid-query-toolkit';
import { aliasFactory, stringFactory } from 'druid-query-toolkit/build/ast/sql-query/helpers';
import React from 'react';
-import { RowFilter } from '../../../query-view';
-
export interface StringMenuItemsProps {
- addFunctionToGroupBy: (
- functionName: string,
- spacing: string[],
- argumentsArray: (StringType | number)[],
- run: boolean,
- alias: Alias,
- ) => void;
- addToGroupBy: (columnName: string, run: boolean) => void;
- addAggregateColumn: (
- columnName: string | RefExpression,
- functionName: string,
- run: boolean,
- alias?: Alias,
- distinct?: boolean,
- filter?: FilterClause,
- ) => void;
- filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
- queryAst?: SqlQuery;
columnName: string;
- clear: (column: string, preferablyRun: boolean) => void;
- hasFilter: boolean;
+ parsedQuery: SqlQuery;
+ onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
}
export class StringMenuItems extends React.PureComponent<StringMenuItemsProps> {
- constructor(props: StringMenuItemsProps, context: any) {
- super(props, context);
- }
-
- renderFilterMenu(): JSX.Element {
- const { columnName, filterByRow } = this.props;
+ renderFilterMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
return (
<MenuItem icon={IconNames.FILTER} text={`Filter`}>
<MenuItem
text={`"${columnName}" = 'xxx'`}
- onClick={() => filterByRow([{ row: 'xxx', header: columnName, operator: '=' }], false)}
+ onClick={() => {
+ onQueryChange(parsedQuery.filterRow(columnName, 'xxx', '='), false);
+ }}
/>
<MenuItem
text={`"${columnName}" LIKE '%xxx%'`}
- onClick={() =>
- filterByRow([{ row: '%xxx%', header: columnName, operator: 'LIKE' }], false)
- }
+ onClick={() => {
+ onQueryChange(parsedQuery.filterRow(columnName, '%xxx%', 'LIKE'), false);
+ }}
/>
</MenuItem>
);
}
- renderRemoveFilter() {
- const { columnName, clear } = this.props;
+ renderRemoveFilter(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.hasFilterForColumn(columnName)) return;
+
return (
<MenuItem
icon={IconNames.FILTER_REMOVE}
text={`Remove filter`}
onClick={() => {
- clear(columnName, true);
+ onQueryChange(parsedQuery.removeFilter(columnName), true);
}}
/>
);
}
- renderGroupByMenu(): JSX.Element {
- const { columnName, addFunctionToGroupBy, addToGroupBy } = this.props;
+ renderGroupByMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.hasGroupBy()) return;
return (
<MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
- <MenuItem text={`"${columnName}"`} onClick={() => addToGroupBy(columnName, true)} />
+ <MenuItem
+ text={`"${columnName}"`}
+ onClick={() => {
+ onQueryChange(parsedQuery.addToGroupBy(columnName), true);
+ }}
+ />
<MenuItem
text={`SUBSTRING("${columnName}", 1, 2) AS "${columnName}_substring"`}
- onClick={() =>
- addFunctionToGroupBy(
- 'SUBSTRING',
- [' ', ' '],
- [stringFactory(columnName, `"`), 1, 2],
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addFunctionToGroupBy(
+ 'SUBSTRING',
+ [' ', ' '],
+ [stringFactory(columnName, `"`), 1, 2],
+
+ aliasFactory(`${columnName}_substring`),
+ ),
true,
- aliasFactory(`${columnName}_substring`),
- )
- }
+ );
+ }}
/>
</MenuItem>
);
}
- renderAggregateMenu(): JSX.Element {
- const { columnName, addAggregateColumn } = this.props;
+ renderAggregateMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.hasGroupBy()) return;
+
return (
<MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
<MenuItem
text={`COUNT(DISTINCT "${columnName}") AS "dist_${columnName}"`}
onClick={() =>
- addAggregateColumn(columnName, 'COUNT', true, aliasFactory(`dist_${columnName}`), true)
+ onQueryChange(
+ parsedQuery.addAggregateColumn(
+ columnName,
+ 'COUNT',
+ aliasFactory(`dist_${columnName}`),
+ ),
+ true,
+ )
}
/>
<MenuItem
text={`COUNT(*) FILTER (WHERE "${columnName}" = 'xxx') AS ${columnName}_filtered_count `}
- onClick={() =>
- addAggregateColumn(
- refExpressionFactory('*'),
- 'COUNT',
- false,
- aliasFactory(`${columnName}_filtered_count`),
- false,
- new FilterClause({
- keyword: 'FILTER',
- spacing: [' '],
- ex: new WhereClause({
- keyword: 'WHERE',
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addAggregateColumn(
+ refExpressionFactory('*'),
+ 'COUNT',
+ aliasFactory(`${columnName}_filtered_count`),
+ false,
+ new FilterClause({
+ keyword: 'FILTER',
spacing: [' '],
- filter: new ComparisonExpression({
- parens: [],
- ex: stringFactory(columnName, '"'),
- rhs: new ComparisonExpressionRhs({
+ ex: new WhereClause({
+ keyword: 'WHERE',
+ spacing: [' '],
+ filter: new ComparisonExpression({
parens: [],
- op: '=',
- rhs: stringFactory('xxx', `'`),
- spacing: [' ', ' '],
+ ex: stringFactory(columnName, '"'),
+ rhs: new ComparisonExpressionRhs({
+ parens: [],
+ op: '=',
+ rhs: stringFactory('xxx', `'`),
+ spacing: [' ', ' '],
+ }),
}),
}),
}),
- }),
- )
- }
+ ),
+ );
+ }}
/>
</MenuItem>
);
}
render(): JSX.Element {
- const { queryAst, hasFilter } = this.props;
- let hasGroupBy;
- if (queryAst) {
- hasGroupBy = queryAst.groupByClause;
- }
return (
<>
- {queryAst && this.renderFilterMenu()}
- {hasFilter && this.renderRemoveFilter()}
- {hasGroupBy && this.renderGroupByMenu()}
- {hasGroupBy && this.renderAggregateMenu()}
+ {this.renderFilterMenu()}
+ {this.renderRemoveFilter()}
+ {this.renderGroupByMenu()}
+ {this.renderAggregateMenu()}
</>
);
}
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
index 7e6725d..3e46135 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
@@ -28,14 +28,9 @@ describe('time menu', () => {
it('matches snapshot', () => {
const timeMenu = (
<TimeMenuItems
- hasFilter
- clear={() => null}
- addFunctionToGroupBy={() => null}
- addToGroupBy={() => null}
- addAggregateColumn={() => null}
- filterByRow={() => null}
columnName={'__time'}
- queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+ parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+ onQueryChange={() => {}}
/>
);
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
index b948d47..231486d 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
@@ -18,14 +18,7 @@
import { MenuDivider, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import {
- AdditiveExpression,
- Alias,
- FilterClause,
- SqlQuery,
- StringType,
- timestampFactory,
-} from 'druid-query-toolkit';
+import { AdditiveExpression, SqlQuery, Timestamp, timestampFactory } from 'druid-query-toolkit';
import {
aliasFactory,
intervalFactory,
@@ -34,107 +27,76 @@ import {
} from 'druid-query-toolkit/build/ast/sql-query/helpers';
import React from 'react';
-import { RowFilter } from '../../../query-view';
-
export interface TimeMenuItemsProps {
- addFunctionToGroupBy: (
- functionName: string,
- spacing: string[],
- argumentsArray: (StringType | number)[],
- run: boolean,
- alias: Alias,
- ) => void;
- addToGroupBy: (columnName: string, run: boolean) => void;
- addAggregateColumn: (
- columnName: string,
- functionName: string,
- run: boolean,
- alias?: Alias,
- distinct?: boolean,
- filter?: FilterClause,
- ) => void;
- filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
- queryAst?: SqlQuery;
columnName: string;
- clear: (column: string, preferablyRun: boolean) => void;
- hasFilter: boolean;
+ parsedQuery: SqlQuery;
+ onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
}
export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
- constructor(props: TimeMenuItemsProps, context: any) {
- super(props, context);
+ static dateToTimestamp(date: Date): Timestamp {
+ return timestampFactory(
+ date
+ .toISOString()
+ .split('.')[0]
+ .split('T')
+ .join(' '),
+ );
+ }
+
+ static floorHour(dt: Date): Date {
+ dt = new Date(dt.valueOf());
+ dt.setUTCMinutes(0, 0, 0);
+ return dt;
+ }
+
+ static nextHour(dt: Date): Date {
+ dt = new Date(dt.valueOf());
+ dt.setUTCHours(dt.getUTCHours() + 1);
+ return dt;
+ }
+
+ static floorDay(dt: Date): Date {
+ dt = new Date(dt.valueOf());
+ dt.setUTCHours(0, 0, 0, 0);
+ return dt;
+ }
+
+ static nextDay(dt: Date): Date {
+ dt = new Date(dt.valueOf());
+ dt.setUTCDate(dt.getUTCDate() + 1);
+ return dt;
}
- formatTime(timePart: number): string {
- if (timePart % 10 > 0) {
- return String(timePart);
- } else return '0' + String(timePart);
+ static floorMonth(dt: Date): Date {
+ dt = new Date(dt.valueOf());
+ dt.setUTCHours(0, 0, 0, 0);
+ dt.setUTCDate(1);
+ return dt;
}
- getNextMonth(month: number, year: number): { month: string; year: number } {
- if (month === 12) {
- return { month: '01', year: year + 1 };
- }
- return { month: this.formatTime(month + 1), year: year };
+ static nextMonth(dt: Date): Date {
+ dt = new Date(dt.valueOf());
+ dt.setUTCMonth(dt.getUTCMonth() + 1);
+ return dt;
}
- getNextDay(
- day: number,
- month: number,
- year: number,
- ): { day: string; month: string; year: number } {
- if (
- month === 1 ||
- month === 3 ||
- month === 5 ||
- month === 7 ||
- month === 8 ||
- month === 10 ||
- month === 12
- ) {
- if (day === 31) {
- const next = this.getNextMonth(month, year);
- return { day: '01', month: next.month, year: next.year };
- }
- } else if (month === 4 || month === 6 || month === 9 || month === 11) {
- if (day === 30) {
- const next = this.getNextMonth(month, year);
- return { day: '01', month: next.month, year: next.year };
- }
- } else if (month === 2) {
- if ((day === 29 && year % 4 === 0) || (day === 28 && year % 4)) {
- const next = this.getNextMonth(month, year);
- return { day: '01', month: next.month, year: next.year };
- }
- }
- return { day: this.formatTime(day + 1), month: this.formatTime(month), year: year };
+ static floorYear(dt: Date): Date {
+ dt = new Date(dt.valueOf());
+ dt.setUTCHours(0, 0, 0, 0);
+ dt.setUTCMonth(0, 1);
+ return dt;
}
- getNextHour(
- hour: number,
- day: number,
- month: number,
- year: number,
- ): { hour: string; day: string; month: string; year: number } {
- if (hour === 23) {
- const next = this.getNextDay(day, month, year);
- return { hour: '00', day: next.day, month: next.month, year: next.year };
- }
- return {
- hour: this.formatTime(hour + 1),
- day: this.formatTime(day),
- month: this.formatTime(month),
- year: year,
- };
+ static nextYear(dt: Date): Date {
+ dt = new Date(dt.valueOf());
+ dt.setUTCFullYear(dt.getUTCFullYear() + 1);
+ return dt;
}
- renderFilterMenu(): JSX.Element {
- const { columnName, filterByRow, clear } = this.props;
- const date = new Date();
- const year = date.getFullYear();
- const month = date.getMonth();
- const day = date.getDay();
- const hour = date.getHours();
+ renderFilterMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ const now = new Date();
return (
<MenuItem icon={IconNames.FILTER} text={`Filter`}>
@@ -147,8 +109,10 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('HOUR', '1')],
spacing: [' ', ' '],
});
- clear(columnName, false);
- filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+ onQueryChange(
+ parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+ true,
+ );
}}
/>
<MenuItem
@@ -160,8 +124,10 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('DAY', '1')],
spacing: [' ', ' '],
});
- clear(columnName, false);
- filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+ onQueryChange(
+ parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+ true,
+ );
}}
/>
<MenuItem
@@ -173,8 +139,10 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('DAY', '7')],
spacing: [' ', ' '],
});
- clear(columnName, false);
- filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+ onQueryChange(
+ parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+ true,
+ );
}}
/>
<MenuItem
@@ -186,8 +154,10 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('MONTH', '1')],
spacing: [' ', ' '],
});
- clear(columnName, false);
- filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+ onQueryChange(
+ parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+ true,
+ );
}}
/>
<MenuItem
@@ -199,33 +169,30 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('YEAR', '1')],
spacing: [' ', ' '],
});
- clear(columnName, false);
- filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+ onQueryChange(
+ parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+ true,
+ );
}}
/>
<MenuDivider />
<MenuItem
text={`Current hour`}
onClick={() => {
- const next = this.getNextHour(hour, day, month, year);
- clear(columnName, false);
- filterByRow(
- [
- {
- row: stringFactory(columnName, `"`),
- header: timestampFactory(
- `${year}-${month}-${day} ${this.formatTime(hour)}:00:00`,
- ),
- operator: '<=',
- },
- {
- row: timestampFactory(
- `${next.year}-${next.month}-${next.day} ${next.hour}:00:00`,
- ),
- header: columnName,
- operator: '<',
- },
- ],
+ const hourStart = TimeMenuItems.floorHour(now);
+ onQueryChange(
+ parsedQuery
+ .removeFilter(columnName)
+ .filterRow(
+ TimeMenuItems.dateToTimestamp(hourStart),
+ stringFactory(columnName, `"`),
+ '<=',
+ )
+ .filterRow(
+ columnName,
+ TimeMenuItems.dateToTimestamp(TimeMenuItems.nextHour(hourStart)),
+ '<',
+ ),
true,
);
}}
@@ -233,21 +200,20 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
<MenuItem
text={`Current day`}
onClick={() => {
- const next = this.getNextDay(day, month, year);
- clear(columnName, false);
- filterByRow(
- [
- {
- row: stringFactory(columnName, `"`),
- header: timestampFactory(`${year}-${month}-${day} 00:00:00`),
- operator: '<=',
- },
- {
- row: timestampFactory(`${next.year}-${next.month}-${next.day} 00:00:00`),
- header: columnName,
- operator: '<',
- },
- ],
+ const dayStart = TimeMenuItems.floorDay(now);
+ onQueryChange(
+ parsedQuery
+ .removeFilter(columnName)
+ .filterRow(
+ TimeMenuItems.dateToTimestamp(dayStart),
+ stringFactory(columnName, `"`),
+ '<=',
+ )
+ .filterRow(
+ columnName,
+ TimeMenuItems.dateToTimestamp(TimeMenuItems.nextDay(dayStart)),
+ '<',
+ ),
true,
);
}}
@@ -255,21 +221,20 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
<MenuItem
text={`Current month`}
onClick={() => {
- const next = this.getNextMonth(month, year);
- clear(columnName, false);
- filterByRow(
- [
- {
- row: stringFactory(columnName, `"`),
- header: timestampFactory(`${year}-${month}-01 00:00:00`),
- operator: '<=',
- },
- {
- row: timestampFactory(`${next.year}-${next.month}-01 00:00:00`),
- header: columnName,
- operator: '<',
- },
- ],
+ const monthStart = TimeMenuItems.floorMonth(now);
+ onQueryChange(
+ parsedQuery
+ .removeFilter(columnName)
+ .filterRow(
+ TimeMenuItems.dateToTimestamp(monthStart),
+ stringFactory(columnName, `"`),
+ '<=',
+ )
+ .filterRow(
+ columnName,
+ TimeMenuItems.dateToTimestamp(TimeMenuItems.nextMonth(monthStart)),
+ '<',
+ ),
true,
);
}}
@@ -277,20 +242,20 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
<MenuItem
text={`Current year`}
onClick={() => {
- clear(columnName, false);
- filterByRow(
- [
- {
- row: stringFactory(columnName, `"`),
- header: timestampFactory(`${year}-01-01 00:00:00`),
- operator: '<=',
- },
- {
- row: timestampFactory(`${Number(year) + 1}-01-01 00:00:00`),
- header: columnName,
- operator: '<',
- },
- ],
+ const yearStart = TimeMenuItems.floorYear(now);
+ onQueryChange(
+ parsedQuery
+ .removeFilter(columnName)
+ .filterRow(
+ TimeMenuItems.dateToTimestamp(yearStart),
+ stringFactory(columnName, `"`),
+ '<=',
+ )
+ .filterRow(
+ columnName,
+ TimeMenuItems.dateToTimestamp(TimeMenuItems.nextYear(yearStart)),
+ '<',
+ ),
true,
);
}}
@@ -299,96 +264,108 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
);
}
- renderRemoveFilter() {
- const { columnName, clear } = this.props;
+ renderRemoveFilter(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.hasFilterForColumn(columnName)) return;
+
return (
<MenuItem
icon={IconNames.FILTER_REMOVE}
text={`Remove filter`}
onClick={() => {
- clear(columnName, true);
+ onQueryChange(parsedQuery.removeFilter(columnName), true);
}}
/>
);
}
- renderGroupByMenu(): JSX.Element {
- const { columnName, addFunctionToGroupBy } = this.props;
+ renderGroupByMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.hasGroupBy()) return;
return (
<MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
<MenuItem
text={`TIME_FLOOR("${columnName}", 'PT1H') AS "${columnName}_time_floor"`}
- onClick={() =>
- addFunctionToGroupBy(
- 'TIME_FLOOR',
- [' '],
- [stringFactory(columnName, `"`), stringFactory('PT1H', `'`)],
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addFunctionToGroupBy(
+ 'TIME_FLOOR',
+ [' '],
+ [stringFactory(columnName, `"`), stringFactory('PT1H', `'`)],
+ aliasFactory(`${columnName}_time_floor`),
+ ),
true,
- aliasFactory(`${columnName}_time_floor`),
- )
- }
+ );
+ }}
/>
<MenuItem
text={`TIME_FLOOR("${columnName}", 'P1D') AS "${columnName}_time_floor"`}
- onClick={() =>
- addFunctionToGroupBy(
- 'TIME_FLOOR',
- [' '],
- [stringFactory(columnName, `"`), stringFactory('P1D', `'`)],
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addFunctionToGroupBy(
+ 'TIME_FLOOR',
+ [' '],
+ [stringFactory(columnName, `"`), stringFactory('P1D', `'`)],
+ aliasFactory(`${columnName}_time_floor`),
+ ),
true,
- aliasFactory(`${columnName}_time_floor`),
- )
- }
+ );
+ }}
/>
<MenuItem
text={`TIME_FLOOR("${columnName}", 'P7D') AS "${columnName}_time_floor"`}
- onClick={() =>
- addFunctionToGroupBy(
- 'TIME_FLOOR',
- [' '],
- [stringFactory(columnName, `"`), stringFactory('P7D', `'`)],
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addFunctionToGroupBy(
+ 'TIME_FLOOR',
+ [' '],
+ [stringFactory(columnName, `"`), stringFactory('P7D', `'`)],
+ aliasFactory(`${columnName}_time_floor`),
+ ),
true,
- aliasFactory(`${columnName}_time_floor`),
- )
- }
+ );
+ }}
/>
</MenuItem>
);
}
- renderAggregateMenu(): JSX.Element {
- const { columnName, addAggregateColumn } = this.props;
+ renderAggregateMenu(): JSX.Element | undefined {
+ const { columnName, parsedQuery, onQueryChange } = this.props;
+ if (!parsedQuery.hasGroupBy()) return;
+
return (
<MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
<MenuItem
text={`MAX("${columnName}") AS "max_${columnName}"`}
- onClick={() =>
- addAggregateColumn(columnName, 'MAX', true, aliasFactory(`max_${columnName}`))
- }
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addAggregateColumn(columnName, 'MAX', aliasFactory(`max_${columnName}`)),
+ true,
+ );
+ }}
/>
<MenuItem
text={`MIN("${columnName}") AS "min_${columnName}"`}
- onClick={() =>
- addAggregateColumn(columnName, 'MIN', true, aliasFactory(`min_${columnName}`))
- }
+ onClick={() => {
+ onQueryChange(
+ parsedQuery.addAggregateColumn(columnName, 'MIN', aliasFactory(`min_${columnName}`)),
+ true,
+ );
+ }}
/>
</MenuItem>
);
}
render(): JSX.Element {
- const { queryAst, hasFilter } = this.props;
- let hasGroupBy;
- if (queryAst) {
- hasGroupBy = queryAst.groupByClause;
- }
return (
<>
- {queryAst && this.renderFilterMenu()}
- {hasFilter && this.renderRemoveFilter()}
- {hasGroupBy && this.renderGroupByMenu()}
- {hasGroupBy && this.renderAggregateMenu()}
+ {this.renderFilterMenu()}
+ {this.renderRemoveFilter()}
+ {this.renderGroupByMenu()}
+ {this.renderAggregateMenu()}
</>
);
}
diff --git a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
index 7856d5e..e925eab 100644
--- a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
@@ -17,6 +17,7 @@
*/
import { render } from '@testing-library/react';
+import { sqlParserFactory } from 'druid-query-toolkit';
import React from 'react';
import { ColumnMetadata } from '../../../utils/column-metadata';
@@ -24,31 +25,38 @@ import { ColumnMetadata } from '../../../utils/column-metadata';
import { ColumnTree } from './column-tree';
describe('column tree', () => {
+ const parser = sqlParserFactory(['COUNT']);
+
it('matches snapshot', () => {
const columnTree = (
<ColumnTree
- queryAst={() => undefined}
- clear={() => null}
- addFunctionToGroupBy={() => null}
- filterByRow={() => null}
- addAggregateColumn={() => null}
- addToGroupBy={() => null}
+ getParsedQuery={() => {
+ return parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`);
+ }}
+ defaultSchema="druid"
+ defaultTable="wikipedia"
columnMetadataLoading={false}
columnMetadata={
[
{
TABLE_SCHEMA: 'druid',
- TABLE_NAME: 'deletion-tutorial',
+ TABLE_NAME: 'wikipedia',
COLUMN_NAME: '__time',
DATA_TYPE: 'TIMESTAMP',
},
{
TABLE_SCHEMA: 'druid',
- TABLE_NAME: 'deletion-tutorial',
+ TABLE_NAME: 'wikipedia',
COLUMN_NAME: 'added',
DATA_TYPE: 'BIGINT',
},
{
+ TABLE_SCHEMA: 'druid',
+ TABLE_NAME: 'wikipedia',
+ COLUMN_NAME: 'addedBy10',
+ DATA_TYPE: 'FLOAT',
+ },
+ {
TABLE_SCHEMA: 'sys',
TABLE_NAME: 'tasks',
COLUMN_NAME: 'error_msg',
@@ -57,7 +65,6 @@ describe('column tree', () => {
] as ColumnMetadata[]
}
onQueryStringChange={() => {}}
- replaceFrom={() => null}
/>
);
diff --git a/web-console/src/views/query-view/column-tree/column-tree.tsx b/web-console/src/views/query-view/column-tree/column-tree.tsx
index 72c3a95..91cfef8 100644
--- a/web-console/src/views/query-view/column-tree/column-tree.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.tsx
@@ -27,26 +27,15 @@ import {
Tree,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import {
- Alias,
- FilterClause,
- RefExpression,
- refExpressionFactory,
- SqlQuery,
- stringFactory,
- StringType,
-} from 'druid-query-toolkit';
+import { refExpressionFactory, SqlQuery, stringFactory } from 'druid-query-toolkit';
import React, { ChangeEvent } from 'react';
import { Loader } from '../../../components';
import { Deferred } from '../../../components/deferred/deferred';
import { copyAndAlert, escapeSqlIdentifier, groupBy } from '../../../utils';
import { ColumnMetadata } from '../../../utils/column-metadata';
-import { RowFilter } from '../query-view';
-import { NumberMenuItems } from './column-tree-menu/number-menu-items/number-menu-items';
-import { StringMenuItems } from './column-tree-menu/string-menu-items/string-menu-items';
-import { TimeMenuItems } from './column-tree-menu/time-menu-items/time-menu-items';
+import { NumberMenuItems, StringMenuItems, TimeMenuItems } from './column-tree-menu';
import './column-tree.scss';
@@ -123,29 +112,10 @@ ORDER BY "Count" DESC`,
export interface ColumnTreeProps {
columnMetadataLoading: boolean;
columnMetadata?: readonly ColumnMetadata[];
- onQueryStringChange: (queryString: string, run: boolean) => void;
+ getParsedQuery: () => SqlQuery | undefined;
+ onQueryStringChange: (queryString: string | SqlQuery, run?: boolean) => void;
defaultSchema?: string;
defaultTable?: string;
- addFunctionToGroupBy: (
- functionName: string,
- spacing: string[],
- argumentsArray: (StringType | number)[],
- run: boolean,
- alias: Alias,
- ) => void;
- addToGroupBy: (columnName: string, run: boolean) => void;
- addAggregateColumn: (
- columnName: string | RefExpression,
- functionName: string,
- run: boolean,
- alias?: Alias,
- distinct?: boolean,
- filter?: FilterClause,
- ) => void;
- filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
- replaceFrom: (table: RefExpression, preferablyRun: boolean) => void;
- queryAst: () => SqlQuery | undefined;
- clear: (column: string, preferablyRun: boolean) => void;
}
export interface ColumnTreeState {
@@ -169,7 +139,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
childNodes: groupBy(
metadata,
r => r.TABLE_NAME,
- (metadata, table) => ({
+ (metadata, table): ITreeNode => ({
id: table,
icon: IconNames.TH,
label: (
@@ -177,149 +147,143 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
boundary={'window'}
position={Position.RIGHT}
content={
- <Menu>
- <MenuItem
- icon={IconNames.FULLSCREEN}
- text={`SELECT ... FROM ${table}`}
- onClick={() => {
- handleTableClick(
- schema,
- {
- id: table,
- icon: IconNames.TH,
- label: table,
- childNodes: metadata.map(columnData => ({
- id: columnData.COLUMN_NAME,
- icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
- label: columnData.COLUMN_NAME,
- })),
- },
- props.onQueryStringChange,
- );
- }}
- />
- <MenuItem
- icon={IconNames.CLIPBOARD}
- text={`Copy: ${table}`}
- onClick={() => {
- copyAndAlert(table, `${table} query copied to clipboard`);
- }}
- />
- <Deferred
- content={() => (
- <>
- {props.queryAst() && (
+ <Deferred
+ content={() => {
+ const parsedQuery = props.getParsedQuery();
+ return (
+ <Menu>
+ <MenuItem
+ icon={IconNames.FULLSCREEN}
+ text={`SELECT ... FROM ${table}`}
+ onClick={() => {
+ handleTableClick(
+ schema,
+ {
+ id: table,
+ icon: IconNames.TH,
+ label: table,
+ childNodes: metadata.map(columnData => ({
+ id: columnData.COLUMN_NAME,
+ icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
+ label: columnData.COLUMN_NAME,
+ })),
+ },
+ props.onQueryStringChange,
+ );
+ }}
+ />
+ <MenuItem
+ icon={IconNames.CLIPBOARD}
+ text={`Copy: ${table}`}
+ onClick={() => {
+ copyAndAlert(table, `${table} query copied to clipboard`);
+ }}
+ />
+ {parsedQuery && (
<MenuItem
icon={IconNames.EXCHANGE}
text={`Replace FROM with: ${table}`}
onClick={() => {
- props.replaceFrom(
- refExpressionFactory(stringFactory(table, `"`)),
+ props.onQueryStringChange(
+ parsedQuery.replaceFrom(
+ refExpressionFactory(stringFactory(table, `"`)),
+ ),
true,
);
}}
/>
)}
- </>
- )}
- />
- </Menu>
+ </Menu>
+ );
+ }}
+ />
}
>
<div>{table}</div>
</Popover>
),
- childNodes: metadata.map(columnData => ({
- id: columnData.COLUMN_NAME,
- icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
- label: (
- <Popover
- boundary={'window'}
- position={Position.RIGHT}
- autoFocus={false}
- targetClassName={'bp3-popover-open'}
- content={
- <Deferred
- content={() => {
- const queryAst = props.queryAst();
- const hasFilter = queryAst
- ? queryAst.getCurrentFilters().includes(columnData.COLUMN_NAME)
- : false;
-
- return (
- <Menu>
- <MenuItem
- icon={IconNames.FULLSCREEN}
- text={`Show: ${columnData.COLUMN_NAME}`}
- onClick={() => {
- handleColumnClick(
- schema,
- table,
- {
- id: columnData.COLUMN_NAME,
- icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
- label: columnData.COLUMN_NAME,
- },
- props.onQueryStringChange,
- );
- }}
- />
- {columnData.DATA_TYPE === 'BIGINT' && (
- <NumberMenuItems
- addFunctionToGroupBy={props.addFunctionToGroupBy}
- addToGroupBy={props.addToGroupBy}
- addAggregateColumn={props.addAggregateColumn}
- filterByRow={props.filterByRow}
- columnName={columnData.COLUMN_NAME}
- queryAst={props.queryAst()}
- clear={props.clear}
- hasFilter={hasFilter}
- />
- )}
- {columnData.DATA_TYPE === 'VARCHAR' && (
- <StringMenuItems
- addFunctionToGroupBy={props.addFunctionToGroupBy}
- addToGroupBy={props.addToGroupBy}
- addAggregateColumn={props.addAggregateColumn}
- filterByRow={props.filterByRow}
- columnName={columnData.COLUMN_NAME}
- queryAst={props.queryAst()}
- clear={props.clear}
- hasFilter={hasFilter}
- />
- )}
- {columnData.DATA_TYPE === 'TIMESTAMP' && (
- <TimeMenuItems
- clear={props.clear}
- addFunctionToGroupBy={props.addFunctionToGroupBy}
- addToGroupBy={props.addToGroupBy}
- addAggregateColumn={props.addAggregateColumn}
- filterByRow={props.filterByRow}
- columnName={columnData.COLUMN_NAME}
- queryAst={props.queryAst()}
- hasFilter={hasFilter}
- />
- )}
- <MenuItem
- icon={IconNames.CLIPBOARD}
- text={`Copy: ${columnData.COLUMN_NAME}`}
- onClick={() => {
- copyAndAlert(
- columnData.COLUMN_NAME,
- `${columnData.COLUMN_NAME} query copied to clipboard`,
- );
- }}
- />
- </Menu>
- );
- }}
- />
- }
- >
- <div>{columnData.COLUMN_NAME}</div>
- </Popover>
+ childNodes: metadata
+ .map(
+ (columnData): ITreeNode => ({
+ id: columnData.COLUMN_NAME,
+ icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
+ label: (
+ <Popover
+ boundary={'window'}
+ position={Position.RIGHT}
+ autoFocus={false}
+ targetClassName={'bp3-popover-open'}
+ content={
+ <Deferred
+ content={() => {
+ const parsedQuery = props.getParsedQuery();
+ return (
+ <Menu>
+ <MenuItem
+ icon={IconNames.FULLSCREEN}
+ text={`Show: ${columnData.COLUMN_NAME}`}
+ onClick={() => {
+ handleColumnClick(
+ schema,
+ table,
+ {
+ id: columnData.COLUMN_NAME,
+ icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
+ label: columnData.COLUMN_NAME,
+ },
+ props.onQueryStringChange,
+ );
+ }}
+ />
+ {parsedQuery &&
+ (columnData.DATA_TYPE === 'BIGINT' ||
+ columnData.DATA_TYPE === 'FLOAT') && (
+ <NumberMenuItems
+ columnName={columnData.COLUMN_NAME}
+ parsedQuery={parsedQuery}
+ onQueryChange={props.onQueryStringChange}
+ />
+ )}
+ {parsedQuery && columnData.DATA_TYPE === 'VARCHAR' && (
+ <StringMenuItems
+ columnName={columnData.COLUMN_NAME}
+ parsedQuery={parsedQuery}
+ onQueryChange={props.onQueryStringChange}
+ />
+ )}
+ {parsedQuery && columnData.DATA_TYPE === 'TIMESTAMP' && (
+ <TimeMenuItems
+ columnName={columnData.COLUMN_NAME}
+ parsedQuery={parsedQuery}
+ onQueryChange={props.onQueryStringChange}
+ />
+ )}
+ <MenuItem
+ icon={IconNames.CLIPBOARD}
+ text={`Copy: ${columnData.COLUMN_NAME}`}
+ onClick={() => {
+ copyAndAlert(
+ columnData.COLUMN_NAME,
+ `${columnData.COLUMN_NAME} query copied to clipboard`,
+ );
+ }}
+ />
+ </Menu>
+ );
+ }}
+ />
+ }
+ >
+ <div>{columnData.COLUMN_NAME}</div>
+ </Popover>
+ ),
+ }),
+ )
+ .sort((a, b) =>
+ String(a.id)
+ .toLowerCase()
+ .localeCompare(String(b.id).toLowerCase()),
),
- })),
}),
),
}),
@@ -370,6 +334,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
case 'VARCHAR':
return IconNames.FONT;
case 'BIGINT':
+ case 'FLOAT':
return IconNames.NUMERICAL;
default:
return IconNames.HELP;
diff --git a/web-console/src/views/query-view/query-output/query-output.spec.tsx b/web-console/src/views/query-view/query-output/query-output.spec.tsx
index 3cfb424..ae3d06e 100644
--- a/web-console/src/views/query-view/query-output/query-output.spec.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.spec.tsx
@@ -24,14 +24,7 @@ import { QueryOutput } from './query-output';
describe('query output', () => {
it('matches snapshot', () => {
const queryOutput = (
- <QueryOutput
- runeMode={false}
- sqlOrderBy={() => null}
- sqlFilterRow={() => null}
- sqlExcludeColumn={() => null}
- loading={false}
- error="lol"
- />
+ <QueryOutput runeMode={false} onQueryChange={() => {}} loading={false} error="lol" />
);
const { container } = render(queryOutput);
diff --git a/web-console/src/views/query-view/query-output/query-output.tsx b/web-console/src/views/query-view/query-output/query-output.tsx
index b56e1bf..17878d5 100644
--- a/web-console/src/views/query-view/query-output/query-output.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.tsx
@@ -28,17 +28,14 @@ import ReactTable from 'react-table';
import { copyAndAlert } from '../../../utils';
import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
-import { RowFilter } from '../query-view';
import './query-output.scss';
export interface QueryOutputProps {
loading: boolean;
- sqlFilterRow: (filters: RowFilter[], run: boolean) => void;
- sqlExcludeColumn: (header: string, run: boolean) => void;
- sqlOrderBy: (header: string, direction: 'ASC' | 'DESC', run: boolean) => void;
queryResult?: HeaderRows;
parsedQuery?: SqlQuery;
+ onQueryChange: (query: SqlQuery, run?: boolean) => void;
error?: string;
runeMode: boolean;
}
@@ -97,7 +94,7 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
);
}
getHeaderActions(h: string) {
- const { parsedQuery, sqlExcludeColumn, sqlOrderBy, runeMode } = this.props;
+ const { parsedQuery, onQueryChange, runeMode } = this.props;
let actionsMenu;
if (parsedQuery) {
@@ -110,7 +107,9 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
basicActions.push({
icon: sorted.desc ? IconNames.SORT_ASC : IconNames.SORT_DESC,
title: `Order by: ${h} ${sorted.desc ? 'ASC' : 'DESC'}`,
- onAction: () => sqlOrderBy(h, sorted.desc ? 'ASC' : 'DESC', true),
+ onAction: () => {
+ onQueryChange(parsedQuery.orderBy(h, sorted.desc ? 'ASC' : 'DESC'), true);
+ },
});
}
});
@@ -120,19 +119,25 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
{
icon: IconNames.SORT_ASC,
title: `Order by: ${h} ASC`,
- onAction: () => sqlOrderBy(h, 'ASC', true),
+ onAction: () => {
+ onQueryChange(parsedQuery.orderBy(h, 'ASC'), true);
+ },
},
{
icon: IconNames.SORT_DESC,
title: `Order by: ${h} DESC`,
- onAction: () => sqlOrderBy(h, 'DESC', true),
+ onAction: () => {
+ onQueryChange(parsedQuery.orderBy(h, 'DESC'), true);
+ },
},
);
}
basicActions.push({
icon: IconNames.CROSS,
title: `Remove: ${h}`,
- onAction: () => sqlExcludeColumn(h, true),
+ onAction: () => {
+ onQueryChange(parsedQuery.excludeColumn(h), true);
+ },
});
actionsMenu = basicActionsToMenu(basicActions);
} else {
@@ -176,7 +181,7 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
}
getRowActions(row: string, header: string) {
- const { parsedQuery, sqlFilterRow, runeMode } = this.props;
+ const { parsedQuery, onQueryChange, runeMode } = this.props;
let actionsMenu;
if (parsedQuery) {
@@ -185,24 +190,32 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
<MenuItem
icon={IconNames.FILTER_KEEP}
text={`Filter by: ${header} = ${row}`}
- onClick={() => sqlFilterRow([{ row, header, operator: '=' }], true)}
+ onClick={() => {
+ onQueryChange(parsedQuery.filterRow(header, row, '='), true);
+ }}
/>
<MenuItem
icon={IconNames.FILTER_REMOVE}
text={`Filter by: ${header} != ${row}`}
- onClick={() => sqlFilterRow([{ row, header, operator: '!=' }], true)}
+ onClick={() => {
+ onQueryChange(parsedQuery.filterRow(header, row, '!='), true);
+ }}
/>
{!isNaN(Number(row)) && (
<>
<MenuItem
icon={IconNames.FILTER_KEEP}
- text={`Filter by: ${header} > ${row}`}
- onClick={() => sqlFilterRow([{ row, header, operator: '>' }], true)}
+ text={`Filter by: ${header} >= ${row}`}
+ onClick={() => {
+ onQueryChange(parsedQuery.filterRow(header, row, '>='), true);
+ }}
/>
<MenuItem
icon={IconNames.FILTER_KEEP}
text={`Filter by: ${header} <= ${row}`}
- onClick={() => sqlFilterRow([{ row, header, operator: '<=' }], true)}
+ onClick={() => {
+ onQueryChange(parsedQuery.filterRow(header, row, '<='), true);
+ }}
/>
</>
)}
diff --git a/web-console/src/views/query-view/query-view.scss b/web-console/src/views/query-view/query-view.scss
index 69d3798..c1a9177 100644
--- a/web-console/src/views/query-view/query-view.scss
+++ b/web-console/src/views/query-view/query-view.scss
@@ -63,6 +63,7 @@ $nav-width: 250px;
margin-right: 15px;
}
+ .auto-run,
.smart-query-limit {
display: inline-block;
margin-bottom: 8px;
diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx
index 22eb5e7..b49a574 100644
--- a/web-console/src/views/query-view/query-view.tsx
+++ b/web-console/src/views/query-view/query-view.tsx
@@ -20,18 +20,12 @@ import { Intent, Switch, Tooltip } from '@blueprintjs/core';
import axios from 'axios';
import classNames from 'classnames';
import {
- AdditiveExpression,
- Alias,
- FilterClause,
HeaderRows,
isFirstRowHeader,
normalizeQueryResult,
- RefExpression,
shouldIncludeTimestamp,
sqlParserFactory,
SqlQuery,
- StringType,
- Timestamp,
} from 'druid-query-toolkit';
import Hjson from 'hjson';
import memoizeOne from 'memoize-one';
@@ -51,8 +45,10 @@ import {
downloadFile,
getDruidErrorMessage,
localStorageGet,
+ localStorageGetJson,
LocalStorageKeys,
localStorageSet,
+ localStorageSetJson,
parseQueryPlan,
queryDruidSql,
QueryManager,
@@ -89,15 +85,9 @@ export interface QueryViewProps {
initQuery: string | undefined;
}
-export interface RowFilter {
- row: string | number | AdditiveExpression | Timestamp | StringType;
- header: string | Timestamp | StringType;
- operator: '!=' | '=' | '>' | '<' | 'like' | '>=' | '<=' | 'LIKE';
-}
-
export interface QueryViewState {
queryString: string;
- queryAst: SqlQuery;
+ parsedQuery: SqlQuery;
queryContext: QueryContext;
wrapQueryLimit: number | undefined;
autoRun: boolean;
@@ -196,32 +186,20 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
super(props, context);
const queryString = props.initQuery || localStorageGet(LocalStorageKeys.QUERY_KEY) || '';
- const queryAst = queryString ? parser(queryString) : undefined;
-
- const localStorageQueryHistory = localStorageGet(LocalStorageKeys.QUERY_HISTORY);
- let queryHistory = [];
- if (localStorageQueryHistory) {
- let possibleQueryHistory: unknown;
- try {
- possibleQueryHistory = JSON.parse(localStorageQueryHistory);
- } catch {}
- if (Array.isArray(possibleQueryHistory)) queryHistory = possibleQueryHistory;
- }
+ const parsedQuery = queryString ? parser(queryString) : undefined;
- const localStorageAutoRun = localStorageGet(LocalStorageKeys.AUTO_RUN);
- let autoRun = true;
- if (localStorageAutoRun) {
- let possibleAutoRun: unknown;
- try {
- possibleAutoRun = JSON.parse(localStorageAutoRun);
- } catch {}
- if (typeof possibleAutoRun === 'boolean') autoRun = possibleAutoRun;
- }
+ const queryContext = localStorageGetJson(LocalStorageKeys.QUERY_CONTEXT) || {};
+
+ const possibleQueryHistory = localStorageGetJson(LocalStorageKeys.QUERY_HISTORY);
+ const queryHistory = Array.isArray(possibleQueryHistory) ? possibleQueryHistory : [];
+
+ const possibleAutoRun = localStorageGetJson(LocalStorageKeys.AUTO_RUN);
+ const autoRun = typeof possibleAutoRun === 'boolean' ? possibleAutoRun : true;
this.state = {
queryString,
- queryAst,
- queryContext: {},
+ parsedQuery,
+ queryContext,
wrapQueryLimit: 100,
autoRun,
@@ -429,7 +407,10 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
return (
<QueryHistoryDialog
queryRecords={queryHistory}
- setQueryString={this.handleQueryStringChange}
+ setQueryString={(queryString, queryContext) => {
+ this.handleQueryContextChange(queryContext);
+ this.handleQueryStringChange(queryString);
+ }}
onClose={() => this.setState({ historyDialogOpen: false })}
/>
);
@@ -441,22 +422,41 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
return (
<EditContextDialog
- onQueryContextChange={(queryContext: QueryContext) =>
- this.setState({ queryContext, editContextDialogOpen: false })
- }
- onClose={() => this.setState({ editContextDialogOpen: false })}
+ onQueryContextChange={this.handleQueryContextChange}
+ onClose={() => {
+ this.setState({ editContextDialogOpen: false });
+ }}
queryContext={queryContext}
/>
);
}
+ renderAutoRunSwitch() {
+ const { autoRun, queryString } = this.state;
+ if (QueryView.isJsonLike(queryString)) return;
+
+ return (
+ <Tooltip
+ content="Automatically run queries when modified via helper action menus."
+ hoverOpenDelay={800}
+ >
+ <Switch
+ className="auto-run"
+ checked={autoRun}
+ label="Auto run"
+ onChange={() => this.handleAutoRunChange(!autoRun)}
+ />
+ </Tooltip>
+ );
+ }
+
renderWrapQueryLimitSelector() {
const { wrapQueryLimit, queryString } = this.state;
if (QueryView.isJsonLike(queryString)) return;
return (
<Tooltip
- content="Automatically wrap the query with a limit to protect against queries with very large result sets"
+ content="Automatically wrap the query with a limit to protect against queries with very large result sets."
hoverOpenDelay={800}
>
<Switch
@@ -470,15 +470,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
}
renderMainArea() {
- const {
- queryString,
- queryContext,
- loading,
- result,
- error,
- columnMetadata,
- autoRun,
- } = this.state;
+ const { queryString, queryContext, loading, result, error, columnMetadata } = this.state;
const emptyQuery = QueryView.isEmptyQuery(queryString);
let currentSchema;
@@ -519,8 +511,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
/>
<div className="control-bar">
<RunButton
- autoRun={autoRun}
- onAutoRunChange={this.handleAutoRunChange}
onEditContext={() => this.setState({ editContextDialogOpen: true })}
runeMode={runeMode}
queryContext={queryContext}
@@ -529,6 +519,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
onExplain={emptyQuery ? undefined : this.handleExplain}
onHistory={() => this.setState({ historyDialogOpen: true })}
/>
+ {this.renderAutoRunSwitch()}
{this.renderWrapQueryLimitSelector()}
{result && (
<QueryExtraInfo
@@ -539,110 +530,23 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
</div>
</div>
<QueryOutput
- sqlExcludeColumn={this.sqlExcludeColumn}
- sqlFilterRow={this.sqlFilterRow}
- sqlOrderBy={this.sqlOrderBy}
runeMode={runeMode}
loading={loading}
+ error={error}
queryResult={result ? result.queryResult : undefined}
parsedQuery={result ? result.parsedQuery : undefined}
- error={error}
+ onQueryChange={this.handleQueryStringChange}
/>
</SplitterLayout>
);
}
- private addFunctionToGroupBy = (
- functionName: string,
- spacing: string[],
- argumentsArray: (StringType | number)[],
- preferablyRun: boolean,
- alias: Alias,
+ private handleQueryStringChange = (
+ queryString: string | SqlQuery,
+ preferablyRun?: boolean,
): void => {
- const { queryAst } = this.state;
- if (!queryAst) return;
-
- const modifiedAst = queryAst.addFunctionToGroupBy(functionName, spacing, argumentsArray, alias);
- this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
- };
-
- private addToGroupBy = (columnName: string, preferablyRun: boolean): void => {
- const { queryAst } = this.state;
- if (!queryAst) return;
-
- const modifiedAst = queryAst.addToGroupBy(columnName);
- this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
- };
-
- private replaceFrom = (table: RefExpression, preferablyRun: boolean): void => {
- const { queryAst } = this.state;
- if (!queryAst) return;
-
- const modifiedAst = queryAst.replaceFrom(table);
- this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
- };
-
- private addAggregateColumn = (
- columnName: string | RefExpression,
- functionName: string,
- preferablyRun: boolean,
- alias?: Alias,
- distinct?: boolean,
- filter?: FilterClause,
- ): void => {
- const { queryAst } = this.state;
- if (!queryAst) return;
-
- const modifiedAst = queryAst.addAggregateColumn(
- columnName,
- functionName,
- alias,
- distinct,
- filter,
- );
- this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
- };
-
- private sqlOrderBy = (
- header: string,
- direction: 'ASC' | 'DESC',
- preferablyRun: boolean,
- ): void => {
- const { queryAst } = this.state;
- if (!queryAst) return;
-
- const modifiedAst = queryAst.orderBy(header, direction);
- this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
- };
-
- private sqlExcludeColumn = (header: string, preferablyRun: boolean): void => {
- const { queryAst } = this.state;
- if (!queryAst) return;
-
- const modifiedAst = queryAst.excludeColumn(header);
- this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
- };
-
- private sqlFilterRow = (filters: RowFilter[], preferablyRun: boolean): void => {
- const { queryAst } = this.state;
- if (!queryAst) return;
-
- let modifiedAst: SqlQuery = queryAst;
- for (const filter of filters) {
- modifiedAst = modifiedAst.filterRow(filter.header, filter.row, filter.operator);
- }
- this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
- };
-
- private sqlClearWhere = (column: string, preferablyRun: boolean): void => {
- const { queryAst } = this.state;
-
- if (!queryAst) return;
- this.handleQueryStringChange(queryAst.removeFilter(column).toString(), preferablyRun);
- };
-
- private handleQueryStringChange = (queryString: string, preferablyRun?: boolean): void => {
- this.setState({ queryString, queryAst: parser(queryString) }, () => {
+ if (queryString instanceof SqlQuery) queryString = queryString.toString();
+ this.setState({ queryString, parsedQuery: parser(queryString) }, () => {
const { autoRun } = this.state;
if (preferablyRun && autoRun) this.handleRun();
});
@@ -654,7 +558,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
private handleAutoRunChange = (autoRun: boolean) => {
this.setState({ autoRun });
- localStorageSet(LocalStorageKeys.AUTO_RUN, String(autoRun));
+ localStorageSetJson(LocalStorageKeys.AUTO_RUN, autoRun);
};
private handleWrapQueryLimitChange = (wrapQueryLimit: number | undefined) => {
@@ -665,10 +569,15 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
const { queryString, queryContext, wrapQueryLimit, queryHistory } = this.state;
if (QueryView.isJsonLike(queryString) && !QueryView.validRune(queryString)) return;
- const newQueryHistory = QueryHistoryDialog.addQueryToHistory(queryHistory, queryString);
+ const newQueryHistory = QueryHistoryDialog.addQueryToHistory(
+ queryHistory,
+ queryString,
+ queryContext,
+ );
- localStorageSet(LocalStorageKeys.QUERY_HISTORY, JSON.stringify(newQueryHistory));
+ localStorageSetJson(LocalStorageKeys.QUERY_HISTORY, newQueryHistory);
localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
+ localStorageSetJson(LocalStorageKeys.QUERY_CONTEXT, queryContext);
this.setState({ queryHistory: newQueryHistory });
this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQueryLimit });
@@ -685,19 +594,19 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
};
- private getQueryAst = () => {
- const { queryAst } = this.state;
- return queryAst;
+ private getParsedQuery = () => {
+ const { parsedQuery } = this.state;
+ return parsedQuery;
};
render(): JSX.Element {
- const { columnMetadata, columnMetadataLoading, columnMetadataError, queryAst } = this.state;
+ const { columnMetadata, columnMetadataLoading, columnMetadataError, parsedQuery } = this.state;
let defaultSchema;
let defaultTable;
- if (queryAst instanceof SqlQuery) {
- defaultSchema = queryAst.getSchema();
- defaultTable = queryAst.getTableName();
+ if (parsedQuery instanceof SqlQuery) {
+ defaultSchema = parsedQuery.getSchema();
+ defaultTable = parsedQuery.getTableName();
}
return (
@@ -706,18 +615,12 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
>
{!columnMetadataError && (
<ColumnTree
- clear={this.sqlClearWhere}
- filterByRow={this.sqlFilterRow}
- addFunctionToGroupBy={this.addFunctionToGroupBy}
- addAggregateColumn={this.addAggregateColumn}
- addToGroupBy={this.addToGroupBy}
- queryAst={this.getQueryAst}
+ getParsedQuery={this.getParsedQuery}
columnMetadataLoading={columnMetadataLoading}
columnMetadata={columnMetadata}
onQueryStringChange={this.handleQueryStringChange}
defaultSchema={defaultSchema ? defaultSchema : 'druid'}
defaultTable={defaultTable}
- replaceFrom={this.replaceFrom}
/>
)}
{this.renderMainArea()}
diff --git a/web-console/src/views/query-view/run-button/run-button.spec.tsx b/web-console/src/views/query-view/run-button/run-button.spec.tsx
index cbe2678..ff0fd77 100644
--- a/web-console/src/views/query-view/run-button/run-button.spec.tsx
+++ b/web-console/src/views/query-view/run-button/run-button.spec.tsx
@@ -25,12 +25,10 @@ describe('run button', () => {
it('matches snapshot', () => {
const runButton = (
<RunButton
- autoRun
- onAutoRunChange={() => {}}
onHistory={() => {}}
onEditContext={() => {}}
runeMode={false}
- queryContext={{}}
+ queryContext={{ f: 3 }}
onQueryContextChange={() => {}}
onRun={() => {}}
onExplain={() => {}}
diff --git a/web-console/src/views/query-view/run-button/run-button.tsx b/web-console/src/views/query-view/run-button/run-button.tsx
index 956b8ac..733d5cf 100644
--- a/web-console/src/views/query-view/run-button/run-button.tsx
+++ b/web-console/src/views/query-view/run-button/run-button.tsx
@@ -33,6 +33,7 @@ import { IconNames } from '@blueprintjs/icons';
import React from 'react';
import { MenuCheckbox } from '../../../components';
+import { pluralIfNeeded } from '../../../utils';
import {
getUseApproximateCountDistinct,
getUseApproximateTopN,
@@ -46,8 +47,6 @@ import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../../../variables';
export interface RunButtonProps {
runeMode: boolean;
- autoRun: boolean;
- onAutoRunChange: (autoRun: boolean) => void;
queryContext: QueryContext;
onQueryContextChange: (newQueryContext: QueryContext) => void;
onRun: (() => void) | undefined;
@@ -58,10 +57,6 @@ export interface RunButtonProps {
@HotkeysTarget
export class RunButton extends React.PureComponent<RunButtonProps> {
- constructor(props: RunButtonProps, context: any) {
- super(props, context);
- }
-
public renderHotkeys() {
return (
<Hotkeys>
@@ -90,31 +85,27 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
onQueryContextChange,
onEditContext,
onHistory,
- autoRun,
- onAutoRunChange,
} = this.props;
const useCache = getUseCache(queryContext);
const useApproximateCountDistinct = getUseApproximateCountDistinct(queryContext);
const useApproximateTopN = getUseApproximateTopN(queryContext);
+ const numContextKeys = Object.keys(queryContext).length;
return (
<Menu>
<MenuItem
icon={IconNames.HELP}
- text="Query docs"
+ text={runeMode ? 'Native query documentation' : 'DruidSQL documentation'}
href={runeMode ? DRUID_DOCS_RUNE : DRUID_DOCS_SQL}
target="_blank"
/>
+ <MenuItem icon={IconNames.HISTORY} text="Query history" onClick={onHistory} />
{!runeMode && (
<>
- {onExplain && <MenuItem icon={IconNames.CLEAN} text="Explain" onClick={onExplain} />}
- <MenuItem icon={IconNames.HISTORY} text="History" onClick={onHistory} />
- <MenuCheckbox
- checked={autoRun}
- label="Auto run queries"
- onChange={() => onAutoRunChange(!autoRun)}
- />
+ {onExplain && (
+ <MenuItem icon={IconNames.CLEAN} text="Explain SQL query" onClick={onExplain} />
+ )}
<MenuCheckbox
checked={useApproximateCountDistinct}
label="Use approximate COUNT(DISTINCT)"
@@ -141,7 +132,12 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
}}
/>
{!runeMode && (
- <MenuItem icon={IconNames.PROPERTIES} text="Edit context" onClick={onEditContext} />
+ <MenuItem
+ icon={IconNames.PROPERTIES}
+ text="Edit context"
+ onClick={onEditContext}
+ labelElement={numContextKeys ? pluralIfNeeded(numContextKeys, 'key') : undefined}
+ />
)}
</Menu>
);
@@ -166,7 +162,7 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
<Button icon={IconNames.CARET_RIGHT} text={runButtonText} disabled />
)}
<Popover position={Position.BOTTOM_LEFT} content={this.renderExtraMenu()}>
- <Button icon={IconNames.MORE} intent={Intent.PRIMARY} />
+ <Button icon={IconNames.MORE} intent={onRun ? Intent.PRIMARY : undefined} />
</Popover>
</ButtonGroup>
);
diff --git a/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap b/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
index a64c9b7..226ad30 100644
--- a/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
+++ b/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
@@ -348,6 +348,12 @@ exports[`tasks view matches snapshot 1`] = `
active={false}
onClick={[Function]}
>
+ Group ID
+ </Blueprint3.Button>
+ <Blueprint3.Button
+ active={false}
+ onClick={[Function]}
+ >
Type
</Blueprint3.Button>
<Blueprint3.Button
@@ -455,6 +461,7 @@ exports[`tasks view matches snapshot 1`] = `
columns={
Array [
"Task ID",
+ "Group ID",
"Type",
"Datasource",
"Location",
@@ -533,6 +540,13 @@ exports[`tasks view matches snapshot 1`] = `
"width": 300,
},
Object {
+ "Aggregated": [Function],
+ "Header": "Group ID",
+ "accessor": "group_id",
+ "show": true,
+ "width": 300,
+ },
+ Object {
"Cell": [Function],
"Header": "Type",
"accessor": "type",
diff --git a/web-console/src/views/task-view/tasks-view.tsx b/web-console/src/views/task-view/tasks-view.tsx
index a02ed72..3b635ce 100644
--- a/web-console/src/views/task-view/tasks-view.tsx
+++ b/web-console/src/views/task-view/tasks-view.tsx
@@ -68,6 +68,7 @@ const supervisorTableColumns: string[] = [
];
const taskTableColumns: string[] = [
'Task ID',
+ 'Group ID',
'Type',
'Datasource',
'Location',
@@ -109,7 +110,7 @@ export interface TasksViewState {
taskFilter: Filter[];
supervisorFilter: Filter[];
- groupTasksBy?: 'type' | 'datasource' | 'status';
+ groupTasksBy?: 'group_id' | 'type' | 'datasource' | 'status';
killTaskId?: string;
@@ -191,7 +192,7 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
};
static TASK_SQL = `SELECT
- "task_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
+ "task_id", "group_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
(
CASE WHEN "status" = 'RUNNING' THEN
@@ -745,6 +746,13 @@ ORDER BY "rank" DESC, "created_time" DESC`;
show: hiddenTaskColumns.exists('Task ID'),
},
{
+ Header: 'Group ID',
+ accessor: 'group_id',
+ width: 300,
+ Aggregated: () => '',
+ show: hiddenTaskColumns.exists('Group ID'),
+ },
+ {
Header: 'Type',
accessor: 'type',
Cell: row => {
@@ -1126,6 +1134,12 @@ ORDER BY "rank" DESC, "created_time" DESC`;
None
</Button>
<Button
+ active={groupTasksBy === 'group_id'}
+ onClick={() => this.setState({ groupTasksBy: 'group_id' })}
+ >
+ Group ID
+ </Button>
+ <Button
active={groupTasksBy === 'type'}
onClick={() => this.setState({ groupTasksBy: 'type' })}
>
diff --git a/web-console/src/visualization/bar-group.tsx b/web-console/src/visualization/bar-group.tsx
index 0856905..50b188c 100644
--- a/web-console/src/visualization/bar-group.tsx
+++ b/web-console/src/visualization/bar-group.tsx
@@ -24,7 +24,7 @@ import { BarUnitData } from '../components/segment-timeline/segment-timeline';
import { BarUnit } from './bar-unit';
import { HoveredBarInfo } from './stacked-bar-chart';
-interface BarGroupProps extends React.Props<any> {
+interface BarGroupProps {
dataToRender: BarUnitData[];
changeActiveDatasource: (e: string) => void;
formatTick: (e: number) => string;
diff --git a/web-console/src/visualization/bar-unit.tsx b/web-console/src/visualization/bar-unit.tsx
index 6c43b1c..aeda205 100644
--- a/web-console/src/visualization/bar-unit.tsx
+++ b/web-console/src/visualization/bar-unit.tsx
@@ -20,7 +20,7 @@ import React from 'react';
import './bar-unit.scss';
-interface BarChartUnitProps extends React.Props<any> {
+interface BarChartUnitProps {
x: number | undefined;
y: number;
width: number;
diff --git a/web-console/src/visualization/chart-axis.tsx b/web-console/src/visualization/chart-axis.tsx
index 294a64b..e71b6ba 100644
--- a/web-console/src/visualization/chart-axis.tsx
+++ b/web-console/src/visualization/chart-axis.tsx
@@ -19,7 +19,7 @@
import * as d3 from 'd3';
import React from 'react';
-interface ChartAxisProps extends React.Props<any> {
+interface ChartAxisProps {
transform: string;
scale: any;
className?: string;
diff --git a/web-console/src/visualization/stacked-bar-chart.tsx b/web-console/src/visualization/stacked-bar-chart.tsx
index 8a96e07..8a6a47e 100644
--- a/web-console/src/visualization/stacked-bar-chart.tsx
+++ b/web-console/src/visualization/stacked-bar-chart.tsx
@@ -27,7 +27,7 @@ import { ChartAxis } from './chart-axis';
import './stacked-bar-chart.scss';
-interface StackedBarChartProps extends React.Props<any> {
+interface StackedBarChartProps {
svgWidth: number;
svgHeight: number;
margin: BarChartMargin;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org