mirror of https://github.com/apache/druid.git
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
This commit is contained in:
parent
496dfa3b15
commit
9254dc8b80
|
@ -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 */
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -36,4 +36,8 @@
|
|||
height: 25vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
.generic-result {
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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-.62.09-.18.22-.32.38-.42.16-.1.35-.15.59-.15.26 0 .47.05.63.14.15.09.28.21.37.35.09.14.15.29.18.45.03.16.05.31.05.45-.01.31-.08.58-.22.82-.14.23-.32.45-.53.65-.22.21-.46.39-.71.57-.26.18-.51.36-.75.54-.5.36-.89.78-1.17 1.27-.28.49-.43 1.06-.44 1.71h5v-1.15H6.43c.05-.17.14-.33.27-.49.13-.15.29-.29.46-.44zm8.5-1.56c-.23-.35-.54-.57-.95-.65v-.02c.34-.13.6-.34.76-.63.16-.29.24-.63.24-1.02 0-.34-.06-.64-.19-.9s-.3-.47-.51-.64c-.21-.17-.45-.3-.72-.38-.27-.09-.54-.13-.82-.13-.36 0-.68.07-.96.2-.28.13-.53.32-.72.55-.2.23-.36.51-.47.83-.11.32-.18.66-.19 1.04h1.15c-.01-.2.01-.39.06-.58.05-.19.12-.36.22-.51.1-.15.22-.27.37-.36.15-.09.32-.13.53-.13.32 0 .59.1.79.3.21.2.31.46.31.79 0 .23-.05.43-.14.59-.09.16-.21.29-.35.38-.15.09-.32.16-.51.19-.19.04-.38.05-.57.04v.93c.23-.01.45 0 .67.02.22.02.42.08.59.17.18.09.32.23.43.4.11.18.16.41.16.71 0 .44-.13.78-.39 1.02s-.58.36-.97.36c-.45 0-.79-.16-1.02-.47-.23-.31-.33-.7-.32-1.17H11c.01.4.06.77.17 1.1.11.33.26.61.47.85.21.23.46.42.77.54.31.13.67.19 1.08.19.34 0 .66-.05.96-.16.3-.11.57-.27.8-.47.23-.2.41-.45.55-.74.13-.27.2-.6.2-.97 0-.5-.11-.92-.34-1.27z"
|
||||
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-.62.09-.18.22-.32.38-.42.16-.1.35-.15.59-.15.26 0 .47.05.63.14.15.09.28.21.37.35.09.14.15.29.18.45.03.16.05.31.05.45-.01.31-.08.58-.22.82-.14.23-.32.45-.53.65-.22.21-.46.39-.71.57-.26.18-.51.36-.75.54-.5.36-.89.78-1.17 1.27-.28.49-.43 1.06-.44 1.71h5v-1.15H6.43c.05-.17.14-.33.27-.49.13-.15.29-.29.46-.44zm8.5-1.56c-.23-.35-.54-.57-.95-.65v-.02c.34-.13.6-.34.76-.63.16-.29.24-.63.24-1.02 0-.34-.06-.64-.19-.9s-.3-.47-.51-.64c-.21-.17-.45-.3-.72-.38-.27-.09-.54-.13-.82-.13-.36 0-.68.07-.96.2-.28.13-.53.32-.72.55-.2.23-.36.51-.47.83-.11.32-.18.66-.19 1.04h1.15c-.01-.2.01-.39.06-.58.05-.19.12-.36.22-.51.1-.15.22-.27.37-.36.15-.09.32-.13.53-.13.32 0 .59.1.79.3.21.2.31.46.31.79 0 .23-.05.43-.14.59-.09.16-.21.29-.35.38-.15.09-.32.16-.51.19-.19.04-.38.05-.57.04v.93c.23-.01.45 0 .67.02.22.02.42.08.59.17.18.09.32.23.43.4.11.18.16.41.16.71 0 .44-.13.78-.39 1.02s-.58.36-.97.36c-.45 0-.79-.16-1.02-.47-.23-.31-.33-.7-.32-1.17H11c.01.4.06.77.17 1.1.11.33.26.61.47.85.21.23.46.42.77.54.31.13.67.19 1.08.19.34 0 .66-.05.96-.16.3-.11.57-.27.8-.47.23-.2.41-.45.55-.74.13-.27.2-.6.2-.97 0-.5-.11-.92-.34-1.27z"
|
||||
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>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './number-menu-items/number-menu-items';
|
||||
export * from './string-menu-items/string-menu-items';
|
||||
export * from './time-menu-items/time-menu-items';
|
|
@ -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={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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(' '),
|
||||
);
|
||||
}
|
||||
|
||||
formatTime(timePart: number): string {
|
||||
if (timePart % 10 > 0) {
|
||||
return String(timePart);
|
||||
} else return '0' + String(timePart);
|
||||
static floorHour(dt: Date): Date {
|
||||
dt = new Date(dt.valueOf());
|
||||
dt.setUTCMinutes(0, 0, 0);
|
||||
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 nextHour(dt: Date): Date {
|
||||
dt = new Date(dt.valueOf());
|
||||
dt.setUTCHours(dt.getUTCHours() + 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 floorDay(dt: Date): Date {
|
||||
dt = new Date(dt.valueOf());
|
||||
dt.setUTCHours(0, 0, 0, 0);
|
||||
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 nextDay(dt: Date): Date {
|
||||
dt = new Date(dt.valueOf());
|
||||
dt.setUTCDate(dt.getUTCDate() + 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();
|
||||
static floorMonth(dt: Date): Date {
|
||||
dt = new Date(dt.valueOf());
|
||||
dt.setUTCHours(0, 0, 0, 0);
|
||||
dt.setUTCDate(1);
|
||||
return dt;
|
||||
}
|
||||
|
||||
static nextMonth(dt: Date): Date {
|
||||
dt = new Date(dt.valueOf());
|
||||
dt.setUTCMonth(dt.getUTCMonth() + 1);
|
||||
return dt;
|
||||
}
|
||||
|
||||
static floorYear(dt: Date): Date {
|
||||
dt = new Date(dt.valueOf());
|
||||
dt.setUTCHours(0, 0, 0, 0);
|
||||
dt.setUTCMonth(0, 1);
|
||||
return dt;
|
||||
}
|
||||
|
||||
static nextYear(dt: Date): Date {
|
||||
dt = new Date(dt.valueOf());
|
||||
dt.setUTCFullYear(dt.getUTCFullYear() + 1);
|
||||
return dt;
|
||||
}
|
||||
|
||||
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()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,30 +25,37 @@ 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',
|
||||
|
@ -57,7 +65,6 @@ describe('column tree', () => {
|
|||
] as ColumnMetadata[]
|
||||
}
|
||||
onQueryStringChange={() => {}}
|
||||
replaceFrom={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -63,6 +63,7 @@ $nav-width: 250px;
|
|||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.auto-run,
|
||||
.smart-query-limit {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
|
|
|
@ -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 parsedQuery = 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 queryContext = localStorageGetJson(LocalStorageKeys.QUERY_CONTEXT) || {};
|
||||
|
||||
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 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()}
|
||||
|
|
|
@ -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={() => {}}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -344,6 +344,12 @@ exports[`tasks view matches snapshot 1`] = `
|
|||
>
|
||||
None
|
||||
</Blueprint3.Button>
|
||||
<Blueprint3.Button
|
||||
active={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Group ID
|
||||
</Blueprint3.Button>
|
||||
<Blueprint3.Button
|
||||
active={false}
|
||||
onClick={[Function]}
|
||||
|
@ -455,6 +461,7 @@ exports[`tasks view matches snapshot 1`] = `
|
|||
columns={
|
||||
Array [
|
||||
"Task ID",
|
||||
"Group ID",
|
||||
"Type",
|
||||
"Datasource",
|
||||
"Location",
|
||||
|
@ -532,6 +539,13 @@ exports[`tasks view matches snapshot 1`] = `
|
|||
"show": true,
|
||||
"width": 300,
|
||||
},
|
||||
Object {
|
||||
"Aggregated": [Function],
|
||||
"Header": "Group ID",
|
||||
"accessor": "group_id",
|
||||
"show": true,
|
||||
"width": 300,
|
||||
},
|
||||
Object {
|
||||
"Cell": [Function],
|
||||
"Header": "Type",
|
||||
|
|
|
@ -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
|
||||
|
@ -744,6 +745,13 @@ ORDER BY "rank" DESC, "created_time" DESC`;
|
|||
Aggregated: () => '',
|
||||
show: hiddenTaskColumns.exists('Task ID'),
|
||||
},
|
||||
{
|
||||
Header: 'Group ID',
|
||||
accessor: 'group_id',
|
||||
width: 300,
|
||||
Aggregated: () => '',
|
||||
show: hiddenTaskColumns.exists('Group ID'),
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
|
@ -1125,6 +1133,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' })}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue