mirror of https://github.com/apache/druid.git
Web-Console: add more side column functions to query view (#8283)
* small fixes * run jest -u * add trim * add to groupby * save * add functionality to column tree menu * remove history dialog * rename file * fixes * add new values to time-menu * add query dialog * add more functions to collumn-tree menu * add menu functions * remove test * update tests, fix extra 0s, clear in between time filters * fixes * add deffered * save * add alias's * change menu style * fix substring * small fixes * add auto run to run button * move divider to a reasonable position * fix auto expland * run jest -u * fix auto run * prevent aggregate cooumn changes * fix group by bug * add quertAst to state * ui fixes * prevent filter on non existant ast * add auto run to shows * use handle run * handle * * add alias and tslint fix
This commit is contained in:
parent
ff26fcb159
commit
8924d285dc
|
@ -4395,9 +4395,9 @@
|
|||
"integrity": "sha512-0sYnfUHHMoajaud/i5BHKA12bUxiWEHJ9rxGqVEppFxsEcxef0TZQ5J59lU+UniEBcz/sG5fTESRyS7cOm3tSQ=="
|
||||
},
|
||||
"druid-query-toolkit": {
|
||||
"version": "0.3.15",
|
||||
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.15.tgz",
|
||||
"integrity": "sha512-q7uKfUdBItjOyNF1PlWF/rAhOim1uAjI085fsoKIBDZ2o5O4XRjaCKqXtW49Ovv92ks/22zLoYWNdU51i4PB/w==",
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.20.tgz",
|
||||
"integrity": "sha512-jrGNu+o/nD+uhbxAMLXEQrSWNEylCRmkiuFDJSPCMz7cjMNArsdIgyBQHPezNAeTDpReelAt59xJ7pvqXwPIvw==",
|
||||
"requires": {
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
"d3": "^5.9.7",
|
||||
"d3-array": "^2.2.0",
|
||||
"druid-console": "^0.0.2",
|
||||
"druid-query-toolkit": "^0.3.15",
|
||||
"druid-query-toolkit": "^0.3.20",
|
||||
"file-saver": "^2.0.2",
|
||||
"has-own-prop": "^2.0.0",
|
||||
"hjson": "^3.1.2",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`deferred matches snapshot 1`] = `<div />`;
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Deferred } from './deferred';
|
||||
|
||||
describe('deferred', () => {
|
||||
it('matches snapshot', () => {
|
||||
const deferred = <Deferred content={() => <div />} />;
|
||||
|
||||
const { container } = render(deferred);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export interface DeferredProps {
|
||||
content: () => JSX.Element;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`query plan dialog matches snapshot 1`] = `
|
||||
<div
|
||||
class="bp3-portal"
|
||||
>
|
||||
<div
|
||||
class="bp3-overlay bp3-overlay-open bp3-overlay-scroll-container"
|
||||
>
|
||||
<div
|
||||
class="bp3-overlay-backdrop bp3-overlay-appear bp3-overlay-appear-active"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
class="bp3-dialog-container bp3-overlay-content bp3-overlay-appear bp3-overlay-appear-active"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog query-history-dialog"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog-header"
|
||||
>
|
||||
<h4
|
||||
class="bp3-heading"
|
||||
>
|
||||
Query history
|
||||
</h4>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="bp3-button bp3-minimal bp3-dialog-close-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-small-cross"
|
||||
icon="small-cross"
|
||||
>
|
||||
<svg
|
||||
data-icon="small-cross"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<desc>
|
||||
small-cross
|
||||
</desc>
|
||||
<path
|
||||
d="M11.41 10l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L11.41 10z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="bp3-dialog-body"
|
||||
>
|
||||
<div
|
||||
class="bp3-tabs bp3-vertical tab-area"
|
||||
>
|
||||
<div
|
||||
class="bp3-tab-list"
|
||||
role="tablist"
|
||||
>
|
||||
<div
|
||||
class="bp3-tab-indicator-wrapper"
|
||||
style="display: none;"
|
||||
>
|
||||
<div
|
||||
class="bp3-tab-indicator"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="bp3-flex-expander"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bp3-dialog-footer"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog-footer-actions"
|
||||
>
|
||||
<button
|
||||
class="bp3-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bp3-button-text"
|
||||
>
|
||||
Close
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="bp3-button bp3-intent-primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bp3-button-text"
|
||||
>
|
||||
Open
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.query-history-dialog {
|
||||
&.bp3-dialog {
|
||||
width: 900px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-area {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
resize: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Classes, Dialog, Intent, Tab, Tabs, TextArea } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
|
||||
import './query-history-dialog.scss';
|
||||
|
||||
export interface QueryRecord {
|
||||
version: string;
|
||||
queryString: string;
|
||||
}
|
||||
export interface QueryHistoryDialogProps {
|
||||
setQueryString: (queryString: string) => void;
|
||||
onClose: () => void;
|
||||
queryRecords: QueryRecord[];
|
||||
}
|
||||
|
||||
export interface QueryHistoryDialogState {
|
||||
activeTab: number;
|
||||
}
|
||||
|
||||
export class QueryHistoryDialog extends React.PureComponent<
|
||||
QueryHistoryDialogProps,
|
||||
QueryHistoryDialogState
|
||||
> {
|
||||
constructor(props: QueryHistoryDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
};
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { onClose, queryRecords, setQueryString } = this.props;
|
||||
const { activeTab } = this.state;
|
||||
|
||||
const versions = queryRecords.map((record, index) => (
|
||||
<Tab
|
||||
id={index}
|
||||
key={index}
|
||||
title={record.version}
|
||||
panel={<TextArea readOnly value={record.queryString} className={'text-area'} />}
|
||||
panelClassName={'panel'}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Dialog className="query-history-dialog" isOpen onClose={onClose} title="Query history">
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<Tabs
|
||||
animate
|
||||
renderActiveTabPanelOnly
|
||||
vertical
|
||||
className={'tab-area'}
|
||||
selectedTabId={activeTab}
|
||||
onChange={(tab: number) => this.setState({ activeTab: tab })}
|
||||
>
|
||||
{versions}
|
||||
<Tabs.Expander />
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button text="Close" onClick={onClose} />
|
||||
<Button
|
||||
text="Open"
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => setQueryString(queryRecords[activeTab].queryString)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { QueryHistoryDialog } from './query-history-dialog';
|
||||
|
||||
describe('query plan dialog', () => {
|
||||
it('matches snapshot', () => {
|
||||
const queryPlanDialog = (
|
||||
<QueryHistoryDialog setQueryString={() => null} queryRecords={[]} onClose={() => {}} />
|
||||
);
|
||||
render(queryPlanDialog);
|
||||
expect(document.body.lastChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -75,6 +75,16 @@ 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>
|
||||
|
|
|
@ -24,7 +24,12 @@ import { QueryPlanDialog } from './query-plan-dialog';
|
|||
describe('query plan dialog', () => {
|
||||
it('matches snapshot', () => {
|
||||
const queryPlanDialog = (
|
||||
<QueryPlanDialog explainResult={'test'} explainError={undefined} onClose={() => {}} />
|
||||
<QueryPlanDialog
|
||||
setQueryString={() => null}
|
||||
explainResult={'test'}
|
||||
explainError={undefined}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
);
|
||||
render(queryPlanDialog);
|
||||
expect(document.body.lastChild).toMatchSnapshot();
|
||||
|
|
|
@ -16,7 +16,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Classes, Dialog, FormGroup, InputGroup, TextArea } from '@blueprintjs/core';
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
Dialog,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Intent,
|
||||
TextArea,
|
||||
} from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
|
||||
import { BasicQueryExplanation, SemiJoinQueryExplanation } from '../../utils';
|
||||
|
@ -27,6 +35,7 @@ export interface QueryPlanDialogProps {
|
|||
explainResult?: BasicQueryExplanation | SemiJoinQueryExplanation | string;
|
||||
explainError?: string;
|
||||
onClose: () => void;
|
||||
setQueryString: (queryString: string) => void;
|
||||
}
|
||||
|
||||
export interface QueryPlanDialogState {}
|
||||
|
@ -40,8 +49,10 @@ export class QueryPlanDialog extends React.PureComponent<
|
|||
this.state = {};
|
||||
}
|
||||
|
||||
private queryString: string = '';
|
||||
|
||||
render(): JSX.Element {
|
||||
const { explainResult, explainError, onClose } = this.props;
|
||||
const { explainResult, explainError, onClose, setQueryString } = this.props;
|
||||
|
||||
let content: JSX.Element;
|
||||
|
||||
|
@ -60,17 +71,15 @@ export class QueryPlanDialog extends React.PureComponent<
|
|||
);
|
||||
}
|
||||
|
||||
this.queryString = JSON.stringify(
|
||||
(explainResult as BasicQueryExplanation).query[0],
|
||||
undefined,
|
||||
2,
|
||||
);
|
||||
content = (
|
||||
<div className="one-query">
|
||||
<FormGroup label="Query">
|
||||
<TextArea
|
||||
readOnly
|
||||
value={JSON.stringify(
|
||||
(explainResult as BasicQueryExplanation).query[0],
|
||||
undefined,
|
||||
2,
|
||||
)}
|
||||
/>
|
||||
<TextArea readOnly value={this.queryString} />
|
||||
</FormGroup>
|
||||
{signature}
|
||||
</div>
|
||||
|
@ -136,6 +145,11 @@ 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)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
|
|
@ -34,6 +34,8 @@ export const LocalStorageKeys = {
|
|||
SERVERS_REFRESH_RATE: 'servers-refresh-rate' as 'servers-refresh-rate',
|
||||
SUPERVISORS_REFRESH_RATE: 'supervisors-refresh-rate' as 'supervisors-refresh-rate',
|
||||
LOOKUPS_REFRESH_RATE: 'lookups-refresh-rate' as 'lookups-refresh-rate',
|
||||
QUERY_HISTORY: 'query-history' as 'query-history',
|
||||
AUTO_RUN: 'auto-run' as 'auto-run',
|
||||
};
|
||||
export type LocalStorageKeys = typeof LocalStorageKeys[keyof typeof LocalStorageKeys];
|
||||
|
||||
|
|
|
@ -5,8 +5,16 @@ 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]}
|
||||
hasGroupBy={[Function]}
|
||||
onQueryStringChange={[Function]}
|
||||
queryAst={[Function]}
|
||||
/>
|
||||
<t
|
||||
customClassName=""
|
||||
|
@ -32,17 +40,19 @@ exports[`sql view matches snapshot 1`] = `
|
|||
className="control-bar"
|
||||
>
|
||||
<HotkeysTarget(RunButton)
|
||||
autoRun={true}
|
||||
onEditContext={[Function]}
|
||||
onExplain={[Function]}
|
||||
onHistory={[Function]}
|
||||
onQueryContextChange={[Function]}
|
||||
onRun={[Function]}
|
||||
queryContext={Object {}}
|
||||
runeMode={false}
|
||||
setAutoRun={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<QueryOutput
|
||||
disabled={true}
|
||||
loading={false}
|
||||
runeMode={false}
|
||||
sqlExcludeColumn={[Function]}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`number menu matches snapshot 1`] = `null`;
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { NumberMenuItems } from './number-menu-items';
|
||||
|
||||
describe('number menu', () => {
|
||||
it('matches snapshot', () => {
|
||||
const numberMenu = (
|
||||
<NumberMenuItems
|
||||
addFunctionToGroupBy={() => null}
|
||||
addToGroupBy={() => null}
|
||||
addAggregateColumn={() => null}
|
||||
filterByRow={() => null}
|
||||
columnName={'text'}
|
||||
/>
|
||||
);
|
||||
|
||||
const { container } = render(numberMenu);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Alias, FilterClause, 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;
|
||||
}
|
||||
|
||||
export class NumberMenuItems extends React.PureComponent<NumberMenuItemsProps> {
|
||||
constructor(props: NumberMenuItemsProps, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
renderFilterMenu(): JSX.Element {
|
||||
const { columnName, filterByRow } = this.props;
|
||||
|
||||
return (
|
||||
<MenuItem icon={IconNames.FILTER} text={`Filter`}>
|
||||
<MenuItem
|
||||
text={`"${columnName}" > 100`}
|
||||
onClick={() => filterByRow([{ row: 100, header: columnName, operator: '>' }], false)}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`"${columnName}" <= 100`}
|
||||
onClick={() => filterByRow([{ row: 100, header: columnName, operator: '<=' }], false)}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderGroupByMenu(): JSX.Element {
|
||||
const { columnName, addFunctionToGroupBy, addToGroupBy } = this.props;
|
||||
|
||||
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,
|
||||
],
|
||||
true,
|
||||
aliasFactory(`${columnName}_truncated`),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderAggregateMenu(): JSX.Element {
|
||||
const { columnName, addAggregateColumn } = this.props;
|
||||
return (
|
||||
<MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
|
||||
<MenuItem
|
||||
text={`SUM(${columnName}) AS "sum_${columnName}"`}
|
||||
onClick={() =>
|
||||
addAggregateColumn(columnName, 'SUM', true, aliasFactory(`sum_${columnName}`))
|
||||
}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`MAX(${columnName}) AS "max_${columnName}"`}
|
||||
onClick={() =>
|
||||
addAggregateColumn(columnName, 'MAX', true, aliasFactory(`max_${columnName}`))
|
||||
}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`MIN(${columnName}) AS "min_${columnName}"`}
|
||||
onClick={() =>
|
||||
addAggregateColumn(columnName, 'MIN', true, aliasFactory(`min_${columnName}`))
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { queryAst } = this.props;
|
||||
let hasGroupBy;
|
||||
if (queryAst) {
|
||||
hasGroupBy = queryAst.groupByClause;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{queryAst && this.renderFilterMenu()}
|
||||
{hasGroupBy && this.renderGroupByMenu()}
|
||||
{hasGroupBy && this.renderAggregateMenu()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`string menu matches snapshot 1`] = `null`;
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { StringMenuItems } from './string-menu-items';
|
||||
|
||||
describe('string menu', () => {
|
||||
it('matches snapshot', () => {
|
||||
const stringMenu = (
|
||||
<StringMenuItems
|
||||
addFunctionToGroupBy={() => null}
|
||||
addToGroupBy={() => null}
|
||||
addAggregateColumn={() => null}
|
||||
filterByRow={() => null}
|
||||
columnName={'text'}
|
||||
/>
|
||||
);
|
||||
|
||||
const { container } = render(stringMenu);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { 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;
|
||||
}
|
||||
|
||||
export class StringMenuItems extends React.PureComponent<StringMenuItemsProps> {
|
||||
constructor(props: StringMenuItemsProps, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
renderFilterMenu(): JSX.Element {
|
||||
const { columnName, filterByRow } = this.props;
|
||||
|
||||
return (
|
||||
<MenuItem icon={IconNames.FILTER} text={`Filter`}>
|
||||
<MenuItem
|
||||
text={`"${columnName}" = 'xxx'`}
|
||||
onClick={() => filterByRow([{ row: 'xxx', header: columnName, operator: '=' }], false)}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`"${columnName}" LIKE '%xxx%'`}
|
||||
onClick={() => filterByRow([{ row: 'xxx', header: columnName, operator: 'LIKE' }], false)}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderGroupByMenu(): JSX.Element {
|
||||
const { columnName, addFunctionToGroupBy, addToGroupBy } = this.props;
|
||||
|
||||
return (
|
||||
<MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
|
||||
<MenuItem text={`"${columnName}"`} onClick={() => addToGroupBy(columnName, true)} />
|
||||
<MenuItem
|
||||
text={`SUBSTRING("${columnName}", 1, 2) AS "${columnName}_substring"`}
|
||||
onClick={() =>
|
||||
addFunctionToGroupBy(
|
||||
'SUBSTRING',
|
||||
[' ', ' '],
|
||||
[stringFactory(columnName, `"`), 1, 2],
|
||||
true,
|
||||
aliasFactory(`${columnName}_substring`),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderAggregateMenu(): JSX.Element {
|
||||
const { columnName, addAggregateColumn } = this.props;
|
||||
return (
|
||||
<MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
|
||||
<MenuItem
|
||||
text={`COUNT(DISTINCT "${columnName}") AS "dist_${columnName}"`}
|
||||
onClick={() =>
|
||||
addAggregateColumn(columnName, 'COUNT', true, 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',
|
||||
spacing: [' '],
|
||||
filter: new ComparisonExpression({
|
||||
parens: [],
|
||||
ex: stringFactory(columnName, '"'),
|
||||
rhs: new ComparisonExpressionRhs({
|
||||
parens: [],
|
||||
op: '=',
|
||||
rhs: stringFactory('xxx', `'`),
|
||||
spacing: [' ', ' '],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { queryAst } = this.props;
|
||||
let hasGroupBy;
|
||||
if (queryAst) {
|
||||
hasGroupBy = queryAst.groupByClause;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{queryAst && this.renderFilterMenu()}
|
||||
{hasGroupBy && this.renderGroupByMenu()}
|
||||
{hasGroupBy && this.renderAggregateMenu()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`time menu matches snapshot 1`] = `null`;
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TimeMenuItems } from './time-menu-items';
|
||||
|
||||
describe('time menu', () => {
|
||||
it('matches snapshot', () => {
|
||||
const timeMenu = (
|
||||
<TimeMenuItems
|
||||
clear={() => null}
|
||||
addFunctionToGroupBy={() => null}
|
||||
addToGroupBy={() => null}
|
||||
addAggregateColumn={() => null}
|
||||
filterByRow={() => null}
|
||||
columnName={'text'}
|
||||
/>
|
||||
);
|
||||
|
||||
const { container } = render(timeMenu);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,380 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MenuDivider, MenuItem } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import {
|
||||
AdditiveExpression,
|
||||
Alias,
|
||||
FilterClause,
|
||||
SqlQuery,
|
||||
StringType,
|
||||
timestampFactory,
|
||||
} from 'druid-query-toolkit';
|
||||
import {
|
||||
aliasFactory,
|
||||
intervalFactory,
|
||||
refExpressionFactory,
|
||||
stringFactory,
|
||||
} 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: () => void;
|
||||
}
|
||||
|
||||
export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
|
||||
constructor(props: TimeMenuItemsProps, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
formatTime(timePart: number): string {
|
||||
if (timePart % 10 > 0) {
|
||||
return String(timePart);
|
||||
} else return '0' + String(timePart);
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return (
|
||||
<MenuItem icon={IconNames.FILTER} text={`Filter`}>
|
||||
<MenuItem
|
||||
text={`Latest hour`}
|
||||
onClick={() => {
|
||||
const additiveExpression = new AdditiveExpression({
|
||||
parens: [],
|
||||
op: ['-'],
|
||||
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('HOUR', '1')],
|
||||
spacing: [' ', ' '],
|
||||
});
|
||||
clear();
|
||||
filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`Latest day`}
|
||||
onClick={() => {
|
||||
const additiveExpression = new AdditiveExpression({
|
||||
parens: [],
|
||||
op: ['-'],
|
||||
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('DAY', '1')],
|
||||
spacing: [' ', ' '],
|
||||
});
|
||||
clear();
|
||||
filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`Latest week`}
|
||||
onClick={() => {
|
||||
const additiveExpression = new AdditiveExpression({
|
||||
parens: [],
|
||||
op: ['-'],
|
||||
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('DAY', '7')],
|
||||
spacing: [' ', ' '],
|
||||
});
|
||||
clear();
|
||||
filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`Latest month`}
|
||||
onClick={() => {
|
||||
const additiveExpression = new AdditiveExpression({
|
||||
parens: [],
|
||||
op: ['-'],
|
||||
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('MONTH', '1')],
|
||||
spacing: [' ', ' '],
|
||||
});
|
||||
clear();
|
||||
filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`Latest year`}
|
||||
onClick={() => {
|
||||
const additiveExpression = new AdditiveExpression({
|
||||
parens: [],
|
||||
op: ['-'],
|
||||
ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('YEAR', '1')],
|
||||
spacing: [' ', ' '],
|
||||
});
|
||||
clear();
|
||||
filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
|
||||
}}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
text={`Current hour`}
|
||||
onClick={() => {
|
||||
const next = this.getNextHour(hour, day, month, year);
|
||||
clear();
|
||||
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: '<',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`Current day`}
|
||||
onClick={() => {
|
||||
const next = this.getNextDay(day, month, year);
|
||||
clear();
|
||||
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: '<',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`Current month`}
|
||||
onClick={() => {
|
||||
const next = this.getNextMonth(month, year);
|
||||
clear();
|
||||
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: '<',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`Current year`}
|
||||
onClick={() => {
|
||||
clear();
|
||||
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: '<',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderGroupByMenu(): JSX.Element {
|
||||
const { columnName, addFunctionToGroupBy } = this.props;
|
||||
|
||||
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', `'`)],
|
||||
true,
|
||||
aliasFactory(`${columnName}_time_floor`),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`TIME_FLOOR("${columnName}", 'P1D') AS "${columnName}_time_floor"`}
|
||||
onClick={() =>
|
||||
addFunctionToGroupBy(
|
||||
'TIME_FLOOR',
|
||||
[' '],
|
||||
[stringFactory(columnName, `"`), stringFactory('P1D', `'`)],
|
||||
true,
|
||||
aliasFactory(`${columnName}_time_floor`),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`TIME_FLOOR("${columnName}", 'P7D') AS "${columnName}_time_floor"`}
|
||||
onClick={() =>
|
||||
addFunctionToGroupBy(
|
||||
'TIME_FLOOR',
|
||||
[' '],
|
||||
[stringFactory(columnName, `"`), stringFactory('P7D', `'`)],
|
||||
true,
|
||||
aliasFactory(`${columnName}_time_floor`),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderAggregateMenu(): JSX.Element {
|
||||
const { columnName, addAggregateColumn } = this.props;
|
||||
return (
|
||||
<MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
|
||||
<MenuItem
|
||||
text={`MAX("${columnName}") AS "max_${columnName}"`}
|
||||
onClick={() =>
|
||||
addAggregateColumn(columnName, 'MAX', true, aliasFactory(`max_${columnName}`))
|
||||
}
|
||||
/>
|
||||
<MenuItem
|
||||
text={`MIN("${columnName}") AS "min_${columnName}"`}
|
||||
onClick={() =>
|
||||
addAggregateColumn(columnName, 'MIN', true, aliasFactory(`min_${columnName}`))
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { queryAst } = this.props;
|
||||
let hasGroupBy;
|
||||
if (queryAst) {
|
||||
hasGroupBy = queryAst.groupByClause;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{queryAst && this.renderFilterMenu()}
|
||||
{hasGroupBy && this.renderGroupByMenu()}
|
||||
{hasGroupBy && this.renderAggregateMenu()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,13 @@ describe('column tree', () => {
|
|||
it('matches snapshot', () => {
|
||||
const columnTree = (
|
||||
<ColumnTree
|
||||
queryAst={() => undefined}
|
||||
hasGroupBy={() => false}
|
||||
clear={() => null}
|
||||
addFunctionToGroupBy={() => null}
|
||||
filterByRow={() => null}
|
||||
addAggregateColumn={() => null}
|
||||
addToGroupBy={() => null}
|
||||
columnMetadataLoading={false}
|
||||
columnMetadata={
|
||||
[
|
||||
|
|
|
@ -16,14 +16,29 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { HTMLSelect, IconName, ITreeNode, Menu, MenuItem, Position, Tree } from '@blueprintjs/core';
|
||||
import { Popover } from '@blueprintjs/core/lib/cjs';
|
||||
import {
|
||||
HTMLSelect,
|
||||
IconName,
|
||||
ITreeNode,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Position,
|
||||
Tree,
|
||||
} from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Alias, FilterClause, RefExpression, SqlQuery, StringType } 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 './column-tree.scss';
|
||||
|
||||
|
@ -88,10 +103,31 @@ export interface ColumnTreeProps {
|
|||
onQueryStringChange: (queryString: string) => 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;
|
||||
hasGroupBy: () => boolean;
|
||||
queryAst: () => SqlQuery | undefined;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export interface ColumnTreeState {
|
||||
prevColumnMetadata?: ColumnMetadata[];
|
||||
prevGroupByStatus?: boolean;
|
||||
columnTree?: ITreeNode[];
|
||||
selectedTreeIndex: number;
|
||||
expandedNode: number;
|
||||
|
@ -100,7 +136,6 @@ export interface ColumnTreeState {
|
|||
export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeState> {
|
||||
static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) {
|
||||
const { columnMetadata, defaultSchema, defaultTable } = props;
|
||||
|
||||
if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
|
||||
const columnTree = groupBy(
|
||||
columnMetadata,
|
||||
|
@ -122,7 +157,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
<Menu>
|
||||
<MenuItem
|
||||
icon={IconNames.FULLSCREEN}
|
||||
text={`Show: ${table}`}
|
||||
text={`Select ... from ${table}`}
|
||||
onClick={() => {
|
||||
handleTableClick(
|
||||
schema,
|
||||
|
@ -160,35 +195,72 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
<Popover
|
||||
boundary={'window'}
|
||||
position={Position.RIGHT}
|
||||
autoFocus={false}
|
||||
targetClassName={'bp3-popover-open'}
|
||||
content={
|
||||
<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,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={IconNames.CLIPBOARD}
|
||||
text={`Copy: ${columnData.COLUMN_NAME}`}
|
||||
onClick={() => {
|
||||
copyAndAlert(
|
||||
columnData.COLUMN_NAME,
|
||||
`${columnData.COLUMN_NAME} query copied to clipboard`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
<Deferred
|
||||
content={() => (
|
||||
<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()}
|
||||
/>
|
||||
)}
|
||||
{columnData.DATA_TYPE === 'VARCHAR' && (
|
||||
<StringMenuItems
|
||||
addFunctionToGroupBy={props.addFunctionToGroupBy}
|
||||
addToGroupBy={props.addToGroupBy}
|
||||
addAggregateColumn={props.addAggregateColumn}
|
||||
filterByRow={props.filterByRow}
|
||||
columnName={columnData.COLUMN_NAME}
|
||||
queryAst={props.queryAst()}
|
||||
/>
|
||||
)}
|
||||
{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()}
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
|
@ -203,29 +275,28 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
let selectedTreeIndex = -1;
|
||||
let expandedNode = -1;
|
||||
if (defaultSchema && columnTree) {
|
||||
selectedTreeIndex = columnTree
|
||||
.map(function(x) {
|
||||
return x.id;
|
||||
})
|
||||
.indexOf(defaultSchema);
|
||||
selectedTreeIndex = columnTree.findIndex(x => {
|
||||
return x.id === defaultSchema;
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedTreeIndex > -1) {
|
||||
const treeNodes = columnTree[selectedTreeIndex].childNodes;
|
||||
if (treeNodes) {
|
||||
if (defaultTable) {
|
||||
expandedNode = treeNodes
|
||||
.map(node => {
|
||||
return node.id;
|
||||
})
|
||||
.indexOf(defaultTable);
|
||||
expandedNode = treeNodes.findIndex(node => {
|
||||
return node.id === defaultTable;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
prevColumnMetadata: columnMetadata,
|
||||
columnTree,
|
||||
selectedTreeIndex,
|
||||
expandedNode,
|
||||
prevGroupByStatus: props.hasGroupBy,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -29,7 +29,6 @@ describe('query output', () => {
|
|||
sqlOrderBy={() => null}
|
||||
sqlFilterRow={() => null}
|
||||
sqlExcludeColumn={() => null}
|
||||
disabled={false}
|
||||
loading={false}
|
||||
error="lol"
|
||||
/>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { Menu, MenuItem, Popover } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { HeaderRows } from 'druid-query-toolkit';
|
||||
import { HeaderRows, SqlQuery } from 'druid-query-toolkit';
|
||||
import {
|
||||
basicIdentifierEscape,
|
||||
basicLiteralEscape,
|
||||
|
@ -28,34 +28,40 @@ 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 {
|
||||
aggregateColumns?: string[];
|
||||
disabled: boolean;
|
||||
loading: boolean;
|
||||
sqlFilterRow: (row: string, header: string, operator: '=' | '!=') => void;
|
||||
sqlExcludeColumn: (header: string) => void;
|
||||
sqlOrderBy: (header: string, direction: 'ASC' | 'DESC') => void;
|
||||
sorted?: { id: string; desc: boolean }[];
|
||||
result?: HeaderRows;
|
||||
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;
|
||||
error?: string;
|
||||
runeMode: boolean;
|
||||
}
|
||||
|
||||
export class QueryOutput extends React.PureComponent<QueryOutputProps> {
|
||||
render(): JSX.Element {
|
||||
const { result, loading, error } = this.props;
|
||||
const { queryResult, parsedQuery, loading, error } = this.props;
|
||||
|
||||
let aggregateColumns: string[] | undefined;
|
||||
if (parsedQuery) {
|
||||
aggregateColumns = parsedQuery.getAggregateColumns();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="query-output">
|
||||
<ReactTable
|
||||
data={result ? result.rows : []}
|
||||
data={queryResult ? queryResult.rows : []}
|
||||
loading={loading}
|
||||
noDataText={!loading && result && !result.rows.length ? 'No results' : error || ''}
|
||||
noDataText={
|
||||
!loading && queryResult && !queryResult.rows.length ? 'No queryResults' : error || ''
|
||||
}
|
||||
sortable={false}
|
||||
columns={(result ? result.header : []).map((h: any, i) => {
|
||||
columns={(queryResult ? queryResult.header : []).map((h: any, i) => {
|
||||
return {
|
||||
Header: () => {
|
||||
return (
|
||||
|
@ -80,11 +86,8 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
|
|||
}
|
||||
return value;
|
||||
},
|
||||
className: this.props.aggregateColumns
|
||||
? this.props.aggregateColumns.indexOf(h) > -1
|
||||
? 'aggregate-column'
|
||||
: undefined
|
||||
: undefined,
|
||||
className:
|
||||
aggregateColumns && aggregateColumns.includes(h) ? 'aggregate-column' : undefined,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
|
@ -92,9 +95,45 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
|
|||
);
|
||||
}
|
||||
getHeaderActions(h: string) {
|
||||
const { disabled, sqlExcludeColumn, sqlOrderBy, runeMode } = this.props;
|
||||
const { parsedQuery, sqlExcludeColumn, sqlOrderBy, runeMode } = this.props;
|
||||
|
||||
let actionsMenu;
|
||||
if (disabled) {
|
||||
if (parsedQuery) {
|
||||
const sorted = parsedQuery.getSorted();
|
||||
|
||||
const basicActions: BasicAction[] = [];
|
||||
if (sorted) {
|
||||
sorted.map(sorted => {
|
||||
if (sorted.id === h) {
|
||||
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),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!basicActions.length) {
|
||||
basicActions.push(
|
||||
{
|
||||
icon: IconNames.SORT_ASC,
|
||||
title: `Order by: ${h} ASC`,
|
||||
onAction: () => sqlOrderBy(h, 'ASC', true),
|
||||
},
|
||||
{
|
||||
icon: IconNames.SORT_DESC,
|
||||
title: `Order by: ${h} DESC`,
|
||||
onAction: () => sqlOrderBy(h, 'DESC', true),
|
||||
},
|
||||
);
|
||||
}
|
||||
basicActions.push({
|
||||
icon: IconNames.CROSS,
|
||||
title: `Remove: ${h}`,
|
||||
onAction: () => sqlExcludeColumn(h, true),
|
||||
});
|
||||
actionsMenu = basicActionsToMenu(basicActions);
|
||||
} else {
|
||||
actionsMenu = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
|
@ -130,48 +169,28 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
|
|||
)}
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
const { sorted } = this.props;
|
||||
const basicActions: BasicAction[] = [];
|
||||
if (sorted) {
|
||||
sorted.map(sorted => {
|
||||
if (sorted.id === h) {
|
||||
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'),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!basicActions.length) {
|
||||
basicActions.push(
|
||||
{
|
||||
icon: IconNames.SORT_ASC,
|
||||
title: `Order by: ${h} ASC`,
|
||||
onAction: () => sqlOrderBy(h, 'ASC'),
|
||||
},
|
||||
{
|
||||
icon: IconNames.SORT_DESC,
|
||||
title: `Order by: ${h} DESC`,
|
||||
onAction: () => sqlOrderBy(h, 'DESC'),
|
||||
},
|
||||
);
|
||||
}
|
||||
basicActions.push({
|
||||
icon: IconNames.CROSS,
|
||||
title: `Remove: ${h}`,
|
||||
onAction: () => sqlExcludeColumn(h),
|
||||
});
|
||||
actionsMenu = basicActionsToMenu(basicActions);
|
||||
}
|
||||
return actionsMenu ? actionsMenu : undefined;
|
||||
}
|
||||
|
||||
getRowActions(row: string, header: string) {
|
||||
const { disabled, sqlFilterRow, runeMode } = this.props;
|
||||
const { parsedQuery, sqlFilterRow, runeMode } = this.props;
|
||||
|
||||
let actionsMenu;
|
||||
if (disabled) {
|
||||
if (parsedQuery) {
|
||||
actionsMenu = basicActionsToMenu([
|
||||
{
|
||||
icon: IconNames.FILTER_KEEP,
|
||||
title: `Filter by: ${header} = ${row}`,
|
||||
onAction: () => sqlFilterRow([{ row, header, operator: '=' }], true),
|
||||
},
|
||||
{
|
||||
icon: IconNames.FILTER_REMOVE,
|
||||
title: `Filter by: ${header} != ${row}`,
|
||||
onAction: () => sqlFilterRow([{ row, header, operator: '!=' }], true),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
actionsMenu = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
|
@ -209,38 +228,29 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
|
|||
)}
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
actionsMenu = basicActionsToMenu([
|
||||
{
|
||||
icon: IconNames.FILTER_KEEP,
|
||||
title: `Filter by: ${header} = ${row}`,
|
||||
onAction: () => sqlFilterRow(row, header, '='),
|
||||
},
|
||||
{
|
||||
icon: IconNames.FILTER_REMOVE,
|
||||
title: `Filter by: ${header} != ${row}`,
|
||||
onAction: () => sqlFilterRow(row, header, '!='),
|
||||
},
|
||||
]);
|
||||
}
|
||||
return actionsMenu ? actionsMenu : undefined;
|
||||
}
|
||||
|
||||
getHeaderClassName(h: string) {
|
||||
const { sorted, aggregateColumns } = this.props;
|
||||
const { parsedQuery } = this.props;
|
||||
|
||||
const className = [];
|
||||
className.push(
|
||||
sorted
|
||||
? sorted.map(sorted => {
|
||||
if (parsedQuery) {
|
||||
const sorted = parsedQuery.getSorted();
|
||||
if (sorted) {
|
||||
className.push(
|
||||
sorted.map(sorted => {
|
||||
if (sorted.id === h) {
|
||||
return sorted.desc ? '-sort-desc' : '-sort-asc';
|
||||
}
|
||||
return '';
|
||||
})[0]
|
||||
: undefined,
|
||||
);
|
||||
if (aggregateColumns) {
|
||||
if (aggregateColumns.includes(h)) {
|
||||
})[0],
|
||||
);
|
||||
}
|
||||
|
||||
const aggregateColumns = parsedQuery.getAggregateColumns();
|
||||
if (aggregateColumns && aggregateColumns.includes(h)) {
|
||||
className.push('aggregate-header');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,20 +20,31 @@ import { Intent } 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';
|
||||
import React from 'react';
|
||||
import SplitterLayout from 'react-splitter-layout';
|
||||
|
||||
import { SQL_FUNCTIONS, SyntaxDescription } from '../../../lib/sql-function-doc';
|
||||
import { QueryPlanDialog } from '../../dialogs';
|
||||
import { EditContextDialog } from '../../dialogs/edit-context-dialog/edit-context-dialog';
|
||||
import {
|
||||
QueryHistoryDialog,
|
||||
QueryRecord,
|
||||
} from '../../dialogs/query-history-dialog/query-history-dialog';
|
||||
import { AppToaster } from '../../singletons/toaster';
|
||||
import {
|
||||
BasicQueryExplanation,
|
||||
|
@ -58,12 +69,20 @@ import { RunButton } from './run-button/run-button';
|
|||
|
||||
import './query-view.scss';
|
||||
|
||||
const parser = sqlParserFactory(
|
||||
const parserRaw = sqlParserFactory(
|
||||
SQL_FUNCTIONS.map((sql_function: SyntaxDescription) => {
|
||||
return sql_function.syntax.substr(0, sql_function.syntax.indexOf('('));
|
||||
}),
|
||||
);
|
||||
|
||||
const parser = memoizeOne((sql: string) => {
|
||||
try {
|
||||
return parserRaw(sql);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
interface QueryWithContext {
|
||||
queryString: string;
|
||||
queryContext: QueryContext;
|
||||
|
@ -74,8 +93,15 @@ 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;
|
||||
queryContext: QueryContext;
|
||||
|
||||
columnMetadataLoading: boolean;
|
||||
|
@ -83,8 +109,7 @@ export interface QueryViewState {
|
|||
columnMetadataError?: string;
|
||||
|
||||
loading: boolean;
|
||||
result?: HeaderRows;
|
||||
queryExtraInfo?: QueryExtraInfoData;
|
||||
result?: QueryResult;
|
||||
error?: string;
|
||||
|
||||
explainDialogOpen: boolean;
|
||||
|
@ -94,9 +119,12 @@ export interface QueryViewState {
|
|||
|
||||
defaultSchema?: string;
|
||||
defaultTable?: string;
|
||||
ast?: SqlQuery;
|
||||
|
||||
editContextDialogOpen: boolean;
|
||||
historyDialogOpen: boolean;
|
||||
queryHistory: QueryRecord[];
|
||||
|
||||
autoRun: boolean;
|
||||
}
|
||||
|
||||
interface QueryResult {
|
||||
|
@ -149,8 +177,18 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
|
||||
constructor(props: QueryViewProps, context: any) {
|
||||
super(props, context);
|
||||
|
||||
let queryString: string | undefined;
|
||||
if (props.initQuery) {
|
||||
queryString = props.initQuery;
|
||||
} else if (localStorageGet(LocalStorageKeys.QUERY_KEY)) {
|
||||
queryString = localStorageGet(LocalStorageKeys.QUERY_KEY);
|
||||
}
|
||||
const queryAst = queryString ? parser(queryString) : undefined;
|
||||
|
||||
this.state = {
|
||||
queryString: props.initQuery || localStorageGet(LocalStorageKeys.QUERY_KEY) || '',
|
||||
queryString: queryString ? queryString : '',
|
||||
queryAst,
|
||||
queryContext: {},
|
||||
|
||||
columnMetadataLoading: false,
|
||||
|
@ -161,8 +199,11 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
loadingExplain: false,
|
||||
|
||||
editContextDialogOpen: false,
|
||||
};
|
||||
historyDialogOpen: false,
|
||||
queryHistory: [],
|
||||
|
||||
autoRun: true,
|
||||
};
|
||||
this.metadataQueryManager = new QueryManager({
|
||||
processQuery: async () => {
|
||||
return await queryDruidSql<ColumnMetadata>({
|
||||
|
@ -199,7 +240,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
if (!(ast instanceof SqlQuery)) {
|
||||
ast = undefined;
|
||||
}
|
||||
|
||||
if (QueryView.isJsonLike(queryString)) {
|
||||
jsonQuery = Hjson.parse(queryString);
|
||||
} else {
|
||||
|
@ -250,7 +290,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
shouldIncludeTimestamp(jsonQuery),
|
||||
isFirstRowHeader(jsonQuery),
|
||||
);
|
||||
|
||||
return {
|
||||
queryResult,
|
||||
queryExtraInfo: {
|
||||
|
@ -266,11 +305,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
},
|
||||
onStateChange: ({ result, loading, error }) => {
|
||||
this.setState({
|
||||
result: result ? result.queryResult : undefined,
|
||||
queryExtraInfo: result ? result.queryExtraInfo : undefined,
|
||||
result,
|
||||
loading,
|
||||
error,
|
||||
ast: result ? result.parsedQuery : undefined,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -300,6 +337,28 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
|
||||
componentDidMount(): void {
|
||||
this.metadataQueryManager.runQuery(null);
|
||||
|
||||
const localStorageQueryHistoy = localStorageGet(LocalStorageKeys.QUERY_HISTORY);
|
||||
let queryHistory;
|
||||
if (localStorageQueryHistoy) {
|
||||
try {
|
||||
queryHistory = JSON.parse(localStorageQueryHistoy);
|
||||
} catch {}
|
||||
if (queryHistory) {
|
||||
this.setState({ queryHistory });
|
||||
}
|
||||
}
|
||||
|
||||
const localStorageAutoRun = localStorageGet(LocalStorageKeys.AUTO_RUN);
|
||||
let autoRun;
|
||||
if (localStorageAutoRun) {
|
||||
try {
|
||||
autoRun = JSON.parse(localStorageAutoRun);
|
||||
} catch {}
|
||||
if (typeof autoRun === 'boolean') {
|
||||
this.setState({ autoRun });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
|
@ -311,21 +370,23 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
handleDownload = (filename: string, format: string) => {
|
||||
const { result } = this.state;
|
||||
if (!result) return;
|
||||
const { queryResult } = result;
|
||||
|
||||
let lines: string[] = [];
|
||||
let separator: string = '';
|
||||
|
||||
if (format === 'csv' || format === 'tsv') {
|
||||
separator = format === 'csv' ? ',' : '\t';
|
||||
lines.push(result.header.map(str => QueryView.formatStr(str, format)).join(separator));
|
||||
lines.push(queryResult.header.map(str => QueryView.formatStr(str, format)).join(separator));
|
||||
lines = lines.concat(
|
||||
result.rows.map(r => r.map(cell => QueryView.formatStr(cell, format)).join(separator)),
|
||||
queryResult.rows.map(r => r.map(cell => QueryView.formatStr(cell, format)).join(separator)),
|
||||
);
|
||||
} else {
|
||||
// json
|
||||
lines = result.rows.map(r => {
|
||||
lines = queryResult.rows.map(r => {
|
||||
const outputObject: Record<string, any> = {};
|
||||
for (let k = 0; k < r.length; k++) {
|
||||
const newName = result.header[k];
|
||||
const newName = queryResult.header[k];
|
||||
if (newName) {
|
||||
outputObject[newName] = r[k];
|
||||
}
|
||||
|
@ -347,6 +408,24 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
explainResult={explainResult}
|
||||
explainError={explainError}
|
||||
onClose={() => this.setState({ explainDialogOpen: false })}
|
||||
setQueryString={(queryString: string) =>
|
||||
this.setState({ queryString, explainDialogOpen: false, queryAst: parser(queryString) })
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderHistoryDialog() {
|
||||
const { historyDialogOpen, queryHistory } = this.state;
|
||||
if (!historyDialogOpen) return;
|
||||
|
||||
return (
|
||||
<QueryHistoryDialog
|
||||
queryRecords={queryHistory}
|
||||
setQueryString={queryString =>
|
||||
this.setState({ queryString, queryAst: parser(queryString), historyDialogOpen: false })
|
||||
}
|
||||
onClose={() => this.setState({ historyDialogOpen: false })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -372,13 +451,11 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
queryContext,
|
||||
loading,
|
||||
result,
|
||||
queryExtraInfo,
|
||||
error,
|
||||
columnMetadata,
|
||||
ast,
|
||||
autoRun,
|
||||
} = this.state;
|
||||
const runeMode = QueryView.isJsonLike(queryString);
|
||||
|
||||
return (
|
||||
<SplitterLayout
|
||||
vertical
|
||||
|
@ -399,77 +476,194 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
/>
|
||||
<div className="control-bar">
|
||||
<RunButton
|
||||
autoRun={autoRun}
|
||||
setAutoRun={(autoRun: boolean) => this.setAutoRun(autoRun)}
|
||||
onEditContext={() => this.setState({ editContextDialogOpen: true })}
|
||||
runeMode={runeMode}
|
||||
queryContext={queryContext}
|
||||
onQueryContextChange={this.handleQueryContextChange}
|
||||
onRun={this.handleRun}
|
||||
onExplain={this.handleExplain}
|
||||
onHistory={() => this.setState({ historyDialogOpen: true })}
|
||||
/>
|
||||
{queryExtraInfo && (
|
||||
<QueryExtraInfo queryExtraInfo={queryExtraInfo} onDownload={this.handleDownload} />
|
||||
{result && (
|
||||
<QueryExtraInfo
|
||||
queryExtraInfo={result.queryExtraInfo}
|
||||
onDownload={this.handleDownload}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<QueryOutput
|
||||
aggregateColumns={ast ? ast.getAggregateColumns() : undefined}
|
||||
disabled={!ast}
|
||||
sorted={ast ? ast.getSorted() : undefined}
|
||||
sqlExcludeColumn={this.sqlExcludeColumn}
|
||||
sqlFilterRow={this.sqlFilterRow}
|
||||
sqlOrderBy={this.sqlOrderBy}
|
||||
runeMode={runeMode}
|
||||
loading={loading}
|
||||
result={result}
|
||||
queryResult={result ? result.queryResult : undefined}
|
||||
parsedQuery={result ? result.parsedQuery : undefined}
|
||||
error={error}
|
||||
/>
|
||||
</SplitterLayout>
|
||||
);
|
||||
}
|
||||
|
||||
private sqlOrderBy = (header: string, direction: 'ASC' | 'DESC'): void => {
|
||||
let { ast } = this.state;
|
||||
if (!ast) return;
|
||||
ast = ast.orderBy(header, direction);
|
||||
private addFunctionToGroupBy = (
|
||||
functionName: string,
|
||||
spacing: string[],
|
||||
argumentsArray: (StringType | number)[],
|
||||
preferablyRun: boolean,
|
||||
alias: Alias,
|
||||
): void => {
|
||||
const { autoRun, queryAst } = this.state;
|
||||
if (!queryAst) return;
|
||||
const groupedAst = queryAst.addFunctionToGroupBy(functionName, spacing, argumentsArray, alias);
|
||||
const queryString = groupedAst.toString();
|
||||
this.setState({
|
||||
queryString: ast.toString(),
|
||||
queryString,
|
||||
queryAst: parser(queryString),
|
||||
});
|
||||
this.handleRun(true, ast.toString());
|
||||
if (autoRun && preferablyRun) {
|
||||
this.handleRun(true, queryString);
|
||||
}
|
||||
};
|
||||
|
||||
private sqlExcludeColumn = (header: string): void => {
|
||||
let { ast } = this.state;
|
||||
if (!ast) return;
|
||||
ast = ast.excludeColumn(header);
|
||||
private addToGroupBy = (columnName: string, preferablyRun: boolean): void => {
|
||||
const { autoRun, queryAst } = this.state;
|
||||
if (!queryAst) return;
|
||||
const groupedAst = queryAst.addToGroupBy(columnName);
|
||||
const queryString = groupedAst.toString();
|
||||
this.setState({
|
||||
queryString: ast.toString(),
|
||||
queryString,
|
||||
queryAst: parser(queryString),
|
||||
});
|
||||
this.handleRun(true, ast.toString());
|
||||
if (autoRun && preferablyRun) {
|
||||
this.handleRun(true, queryString);
|
||||
}
|
||||
};
|
||||
|
||||
private sqlFilterRow = (row: string, header: string, operator: '!=' | '='): void => {
|
||||
let { ast } = this.state;
|
||||
if (!ast) return;
|
||||
ast = ast.filterRow(header, row, operator);
|
||||
private addAggregateColumn = (
|
||||
columnName: string | RefExpression,
|
||||
functionName: string,
|
||||
preferablyRun: boolean,
|
||||
alias?: Alias,
|
||||
distinct?: boolean,
|
||||
filter?: FilterClause,
|
||||
): void => {
|
||||
const { autoRun, queryAst } = this.state;
|
||||
if (!queryAst) return;
|
||||
const modifiedAst = queryAst.addAggregateColumn(
|
||||
columnName,
|
||||
functionName,
|
||||
alias,
|
||||
distinct,
|
||||
filter,
|
||||
);
|
||||
const queryString = modifiedAst.toString();
|
||||
this.setState({
|
||||
queryString: ast.toString(),
|
||||
queryString,
|
||||
queryAst: parser(queryString),
|
||||
});
|
||||
this.handleRun(true, ast.toString());
|
||||
if (autoRun && preferablyRun) {
|
||||
this.handleRun(true, queryString);
|
||||
}
|
||||
};
|
||||
|
||||
private sqlOrderBy = (
|
||||
header: string,
|
||||
direction: 'ASC' | 'DESC',
|
||||
preferablyRun: boolean,
|
||||
): void => {
|
||||
const { autoRun, queryAst } = this.state;
|
||||
if (!queryAst) return;
|
||||
const modifiedAst = queryAst.orderBy(header, direction);
|
||||
const queryString = modifiedAst.toString();
|
||||
this.setState({
|
||||
queryString,
|
||||
queryAst: parser(queryString),
|
||||
});
|
||||
if (autoRun && preferablyRun) {
|
||||
this.handleRun(true, queryString);
|
||||
}
|
||||
};
|
||||
|
||||
private sqlExcludeColumn = (header: string, preferablyRun: boolean): void => {
|
||||
const { autoRun, queryAst } = this.state;
|
||||
if (!queryAst) return;
|
||||
const modifiedAst = queryAst.excludeColumn(header);
|
||||
const queryString = modifiedAst.toString();
|
||||
this.setState({
|
||||
queryString,
|
||||
queryAst: parser(queryString),
|
||||
});
|
||||
if (autoRun && preferablyRun) {
|
||||
this.handleRun(true, queryString);
|
||||
}
|
||||
};
|
||||
|
||||
private sqlFilterRow = (filters: RowFilter[], preferablyRun: boolean): void => {
|
||||
const { autoRun, queryAst } = this.state;
|
||||
if (!queryAst) return;
|
||||
|
||||
let modifiedAst: SqlQuery = queryAst;
|
||||
if (queryAst) {
|
||||
for (const filter of filters) {
|
||||
modifiedAst = modifiedAst.filterRow(filter.header, filter.row, filter.operator);
|
||||
}
|
||||
}
|
||||
const queryString = modifiedAst.toString();
|
||||
this.setState({
|
||||
queryString,
|
||||
queryAst: parser(queryString),
|
||||
});
|
||||
if (autoRun && preferablyRun) {
|
||||
this.handleRun(true, queryString);
|
||||
}
|
||||
};
|
||||
|
||||
private sqlClearWhere = (): void => {
|
||||
const { queryAst } = this.state;
|
||||
|
||||
if (!queryAst) return;
|
||||
if (queryAst.whereClause) {
|
||||
queryAst.whereClause = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private handleQueryStringChange = (queryString: string): void => {
|
||||
this.setState({ queryString });
|
||||
this.setState({ queryString, queryAst: parser(queryString) });
|
||||
};
|
||||
|
||||
private handleQueryContextChange = (queryContext: QueryContext) => {
|
||||
this.setState({ queryContext });
|
||||
};
|
||||
|
||||
private setAutoRun = (autoRun: boolean) => {
|
||||
this.setState({ autoRun });
|
||||
localStorageSet(LocalStorageKeys.AUTO_RUN, String(autoRun));
|
||||
};
|
||||
|
||||
private handleRun = (wrapQuery: boolean, customQueryString?: string) => {
|
||||
const { queryString, queryContext } = this.state;
|
||||
const { queryString, queryContext, queryHistory } = this.state;
|
||||
if (!customQueryString) {
|
||||
customQueryString = queryString;
|
||||
}
|
||||
|
||||
while (queryHistory.length > 9) {
|
||||
queryHistory.pop();
|
||||
}
|
||||
queryHistory.unshift({
|
||||
version: `${new Date().toISOString()}`,
|
||||
queryString: customQueryString,
|
||||
});
|
||||
let queryHistoryString;
|
||||
try {
|
||||
queryHistoryString = JSON.stringify(queryHistory);
|
||||
} catch {}
|
||||
if (queryHistoryString) {
|
||||
localStorageSet(LocalStorageKeys.QUERY_HISTORY, queryHistoryString);
|
||||
}
|
||||
|
||||
if (QueryView.isJsonLike(customQueryString) && !QueryView.validRune(customQueryString)) return;
|
||||
|
||||
localStorageSet(LocalStorageKeys.QUERY_KEY, customQueryString);
|
||||
|
@ -486,32 +680,47 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const {
|
||||
columnMetadata,
|
||||
columnMetadataLoading,
|
||||
columnMetadataError,
|
||||
ast,
|
||||
queryString,
|
||||
} = this.state;
|
||||
|
||||
private getGroupBySetting = () => {
|
||||
const { queryString, queryAst } = this.state;
|
||||
const ast = queryAst;
|
||||
let tempAst: SqlQuery | undefined;
|
||||
if (!ast) {
|
||||
try {
|
||||
tempAst = parser(queryString);
|
||||
} catch {}
|
||||
tempAst = parser(queryString);
|
||||
}
|
||||
let defaultSchema;
|
||||
|
||||
let hasGroupBy = false;
|
||||
if (ast && ast instanceof SqlQuery) {
|
||||
defaultSchema = ast.getSchema();
|
||||
hasGroupBy = !!ast.groupByClause;
|
||||
} else if (tempAst && tempAst instanceof SqlQuery) {
|
||||
defaultSchema = tempAst.getSchema();
|
||||
hasGroupBy = !!tempAst.groupByClause;
|
||||
}
|
||||
return hasGroupBy;
|
||||
};
|
||||
|
||||
private getQueryAst = () => {
|
||||
const { queryAst } = this.state;
|
||||
return queryAst;
|
||||
};
|
||||
|
||||
private onQueryStringChange = (queryString: string) => {
|
||||
const { autoRun } = this.state;
|
||||
|
||||
this.handleQueryStringChange(queryString);
|
||||
if (autoRun) {
|
||||
this.handleRun(true, queryString);
|
||||
}
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { columnMetadata, columnMetadataLoading, columnMetadataError, queryAst } = this.state;
|
||||
|
||||
let defaultSchema;
|
||||
if (queryAst && queryAst instanceof SqlQuery) {
|
||||
defaultSchema = queryAst.getSchema();
|
||||
}
|
||||
let defaultTable;
|
||||
if (ast && ast instanceof SqlQuery) {
|
||||
defaultTable = ast.getTableName();
|
||||
} else if (tempAst && tempAst instanceof SqlQuery) {
|
||||
defaultTable = tempAst.getTableName();
|
||||
if (queryAst && queryAst instanceof SqlQuery) {
|
||||
defaultTable = queryAst.getTableName();
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -520,15 +729,23 @@ 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}
|
||||
hasGroupBy={this.getGroupBySetting}
|
||||
queryAst={this.getQueryAst}
|
||||
columnMetadataLoading={columnMetadataLoading}
|
||||
columnMetadata={columnMetadata}
|
||||
onQueryStringChange={this.handleQueryStringChange}
|
||||
defaultSchema={defaultSchema}
|
||||
onQueryStringChange={this.onQueryStringChange}
|
||||
defaultSchema={defaultSchema ? defaultSchema : 'druid'}
|
||||
defaultTable={defaultTable}
|
||||
/>
|
||||
)}
|
||||
{this.renderMainArea()}
|
||||
{this.renderExplainDialog()}
|
||||
{this.renderHistoryDialog()}
|
||||
{this.renderEditContextDialog()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -25,6 +25,9 @@ describe('run button', () => {
|
|||
it('matches snapshot', () => {
|
||||
const runButton = (
|
||||
<RunButton
|
||||
autoRun
|
||||
setAutoRun={() => null}
|
||||
onHistory={() => null}
|
||||
onEditContext={() => null}
|
||||
runeMode={false}
|
||||
queryContext={{}}
|
||||
|
|
|
@ -45,11 +45,14 @@ import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../../../variables';
|
|||
|
||||
export interface RunButtonProps {
|
||||
runeMode: boolean;
|
||||
autoRun: boolean;
|
||||
queryContext: QueryContext;
|
||||
onQueryContextChange: (newQueryContext: QueryContext) => void;
|
||||
onRun: (wrapQuery: boolean) => void;
|
||||
onExplain: () => void;
|
||||
onEditContext: () => void;
|
||||
onHistory: () => void;
|
||||
setAutoRun: (autoRun: boolean) => void;
|
||||
}
|
||||
|
||||
interface RunButtonState {
|
||||
|
@ -86,7 +89,16 @@ export class RunButton extends React.PureComponent<RunButtonProps, RunButtonStat
|
|||
};
|
||||
|
||||
renderExtraMenu() {
|
||||
const { runeMode, onExplain, queryContext, onQueryContextChange, onEditContext } = this.props;
|
||||
const {
|
||||
runeMode,
|
||||
onExplain,
|
||||
queryContext,
|
||||
onQueryContextChange,
|
||||
onEditContext,
|
||||
onHistory,
|
||||
setAutoRun,
|
||||
autoRun,
|
||||
} = this.props;
|
||||
const { wrapQuery } = this.state;
|
||||
|
||||
const useCache = getUseCache(queryContext);
|
||||
|
@ -104,11 +116,17 @@ export class RunButton extends React.PureComponent<RunButtonProps, RunButtonStat
|
|||
{!runeMode && (
|
||||
<>
|
||||
<MenuItem icon={IconNames.CLEAN} text="Explain" onClick={onExplain} />
|
||||
<MenuItem icon={IconNames.HISTORY} text="History" onClick={onHistory} />
|
||||
<MenuCheckbox
|
||||
checked={wrapQuery}
|
||||
label="Wrap query with limit"
|
||||
onChange={() => this.setState({ wrapQuery: !wrapQuery })}
|
||||
/>
|
||||
<MenuCheckbox
|
||||
checked={autoRun}
|
||||
label="Auto run queries"
|
||||
onChange={() => setAutoRun(!autoRun)}
|
||||
/>
|
||||
<MenuCheckbox
|
||||
checked={useApproximateCountDistinct}
|
||||
label="Use approximate COUNT(DISTINCT)"
|
||||
|
|
Loading…
Reference in New Issue