mirror of https://github.com/apache/druid.git
Web console query view improvements (#16991)
* Made maxNumTaskOptions configurable in the Query view * Updated the copy for taskAssignment options * Reordered options in engine menu for msq engine * fixed snapshot * maxNumTaskOptions -> maxTasksOptions * added back select destination item * fixed duplicate menu item * snapshot * Added the ability to hide certain engine menu options * Added the ability to hide/show more menu items * -> fn * -> fn
This commit is contained in:
parent
2427972c10
commit
5de84253d8
|
@ -327,7 +327,6 @@ export class ConsoleApplication extends React.PureComponent<
|
|||
baseQueryContext={baseQueryContext}
|
||||
serverQueryContext={serverQueryContext}
|
||||
queryEngines={queryEngines}
|
||||
allowExplain
|
||||
goToTask={this.goToTasksWithTaskId}
|
||||
getClusterCapacity={maybeGetClusterCapacity}
|
||||
/>,
|
||||
|
|
|
@ -100,14 +100,13 @@ exports[`MaxTasksButton matches snapshot 1`] = `
|
|||
multiline={true}
|
||||
onClick={[Function]}
|
||||
popoverProps={{}}
|
||||
shouldDismissPopover={false}
|
||||
shouldDismissPopover={true}
|
||||
text={
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
Max
|
||||
</strong>
|
||||
:
|
||||
uses the maximum possible tasks up to the specified limit.
|
||||
: uses the maximum possible tasks up to the specified limit.
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
|
@ -115,24 +114,28 @@ exports[`MaxTasksButton matches snapshot 1`] = `
|
|||
active={false}
|
||||
disabled={false}
|
||||
icon="blank"
|
||||
labelElement={
|
||||
<Blueprint5.Button
|
||||
icon="help"
|
||||
minimal={true}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
}
|
||||
multiline={true}
|
||||
onClick={[Function]}
|
||||
popoverProps={{}}
|
||||
shouldDismissPopover={false}
|
||||
shouldDismissPopover={true}
|
||||
text={
|
||||
<React.Fragment>
|
||||
<strong>
|
||||
Auto
|
||||
</strong>
|
||||
:
|
||||
maximizes the number of tasks while staying within 512 MiB or 10,000 files per task, unless more tasks are needed to stay under the max task limit.
|
||||
: uses the minimum number of tasks while
|
||||
|
||||
<span
|
||||
onClick={[Function]}
|
||||
style={
|
||||
{
|
||||
"color": "#3eadf9",
|
||||
"cursor": "pointer",
|
||||
}
|
||||
}
|
||||
>
|
||||
staying within constraints.
|
||||
</span>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -19,34 +19,17 @@
|
|||
import type { ButtonProps } from '@blueprintjs/core';
|
||||
import { Button, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import type { JSX, ReactNode } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { NumericInputDialog } from '../../../dialogs';
|
||||
import type { QueryContext, TaskAssignment } from '../../../druid-models';
|
||||
import type { QueryContext } from '../../../druid-models';
|
||||
import { getQueryContextKey } from '../../../druid-models';
|
||||
import { getLink } from '../../../links';
|
||||
import { capitalizeFirst, deleteKeys, formatInteger, tickIcon } from '../../../utils';
|
||||
|
||||
const MAX_NUM_TASK_OPTIONS = [2, 3, 4, 5, 7, 9, 11, 17, 33, 65, 129];
|
||||
const TASK_ASSIGNMENT_OPTIONS: TaskAssignment[] = ['max', 'auto'];
|
||||
|
||||
const TASK_ASSIGNMENT_DESCRIPTION: Record<string, string> = {
|
||||
max: 'uses the maximum possible tasks up to the specified limit.',
|
||||
auto: 'maximizes the number of tasks while staying within 512 MiB or 10,000 files per task, unless more tasks are needed to stay under the max task limit.',
|
||||
};
|
||||
|
||||
const TASK_ASSIGNMENT_LABEL_ELEMENT: Record<string, ReactNode> = {
|
||||
auto: (
|
||||
<Button
|
||||
icon={IconNames.HELP}
|
||||
minimal
|
||||
onClick={() =>
|
||||
window.open(`${getLink('DOCS')}/multi-stage-query/reference#context-parameters`, '_blank')
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
const DEFAULT_MAX_TASKS_OPTIONS = [2, 3, 4, 5, 7, 9, 11, 17, 33, 65, 129];
|
||||
const TASK_DOCUMENTATION_LINK = `${getLink('DOCS')}/multi-stage-query/reference#context-parameters`;
|
||||
|
||||
const DEFAULT_MAX_NUM_TASKS_LABEL_FN = (maxNum: number) => {
|
||||
if (maxNum === 2) return { text: formatInteger(maxNum), label: '(1 controller + 1 worker)' };
|
||||
|
@ -63,6 +46,7 @@ export interface MaxTasksButtonProps extends Omit<ButtonProps, 'text' | 'rightIc
|
|||
defaultQueryContext: QueryContext;
|
||||
menuHeader?: JSX.Element;
|
||||
maxTasksLabelFn?: (maxNum: number) => { text: string; label?: string };
|
||||
maxTasksOptions?: number[];
|
||||
fullClusterCapacityLabelFn?: (clusterCapacity: number) => string;
|
||||
}
|
||||
|
||||
|
@ -75,6 +59,7 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
|
|||
menuHeader,
|
||||
maxTasksLabelFn = DEFAULT_MAX_NUM_TASKS_LABEL_FN,
|
||||
fullClusterCapacityLabelFn = DEFAULT_FULL_CLUSTER_CAPACITY_LABEL_FN,
|
||||
maxTasksOptions = DEFAULT_MAX_TASKS_OPTIONS,
|
||||
...rest
|
||||
} = props;
|
||||
const [customMaxNumTasksDialogOpen, setCustomMaxNumTasksDialogOpen] = useState(false);
|
||||
|
@ -86,8 +71,8 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
|
|||
typeof clusterCapacity === 'number' ? fullClusterCapacityLabelFn(clusterCapacity) : undefined;
|
||||
|
||||
const shownMaxNumTaskOptions = clusterCapacity
|
||||
? MAX_NUM_TASK_OPTIONS.filter(_ => _ <= clusterCapacity)
|
||||
: MAX_NUM_TASK_OPTIONS;
|
||||
? maxTasksOptions.filter(_ => _ <= clusterCapacity)
|
||||
: maxTasksOptions;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -103,6 +88,7 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
|
|||
icon={tickIcon(typeof maxNumTasks === 'undefined')}
|
||||
text={fullClusterCapacity}
|
||||
onClick={() => changeQueryContext(deleteKeys(queryContext, ['maxNumTasks']))}
|
||||
shouldDismissPopover
|
||||
/>
|
||||
)}
|
||||
{shownMaxNumTaskOptions.map(m => {
|
||||
|
@ -115,6 +101,7 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
|
|||
text={text}
|
||||
label={label}
|
||||
onClick={() => changeQueryContext({ ...queryContext, maxNumTasks: m })}
|
||||
shouldDismissPopover
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -132,21 +119,39 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
|
|||
label={capitalizeFirst(taskAssigment)}
|
||||
submenuProps={{ style: { width: 300 } }}
|
||||
>
|
||||
{TASK_ASSIGNMENT_OPTIONS.map(t => (
|
||||
<MenuItem
|
||||
key={String(t)}
|
||||
icon={tickIcon(t === taskAssigment)}
|
||||
text={
|
||||
<>
|
||||
<strong>{capitalizeFirst(t)}</strong>: {TASK_ASSIGNMENT_DESCRIPTION[t]}
|
||||
</>
|
||||
}
|
||||
labelElement={TASK_ASSIGNMENT_LABEL_ELEMENT[t]}
|
||||
shouldDismissPopover={false}
|
||||
multiline
|
||||
onClick={() => changeQueryContext({ ...queryContext, taskAssignment: t })}
|
||||
/>
|
||||
))}
|
||||
<MenuItem
|
||||
icon={tickIcon(taskAssigment === 'max')}
|
||||
text={
|
||||
<>
|
||||
<strong>Max</strong>: uses the maximum possible tasks up to the specified limit.
|
||||
</>
|
||||
}
|
||||
multiline
|
||||
onClick={() => changeQueryContext({ ...queryContext, taskAssignment: 'max' })}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
icon={tickIcon(taskAssigment === 'auto')}
|
||||
text={
|
||||
<>
|
||||
<strong>Auto</strong>: uses the minimum number of tasks while{' '}
|
||||
<span
|
||||
style={{
|
||||
color: '#3eadf9',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={e => {
|
||||
window.open(TASK_DOCUMENTATION_LINK, '_blank');
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
staying within constraints.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
multiline
|
||||
onClick={() => changeQueryContext({ ...queryContext, taskAssignment: 'auto' })}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
}
|
||||
|
|
|
@ -73,7 +73,12 @@ const queryRunner = new QueryRunner({
|
|||
export interface QueryTabProps
|
||||
extends Pick<
|
||||
RunPanelProps,
|
||||
'maxTasksMenuHeader' | 'enginesLabelFn' | 'maxTasksLabelFn' | 'fullClusterCapacityLabelFn'
|
||||
| 'maxTasksMenuHeader'
|
||||
| 'enginesLabelFn'
|
||||
| 'maxTasksLabelFn'
|
||||
| 'fullClusterCapacityLabelFn'
|
||||
| 'maxTasksOptions'
|
||||
| 'hiddenOptions'
|
||||
> {
|
||||
query: WorkbenchQuery;
|
||||
id: string;
|
||||
|
@ -110,7 +115,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
maxTasksMenuHeader,
|
||||
enginesLabelFn,
|
||||
maxTasksLabelFn,
|
||||
maxTasksOptions,
|
||||
fullClusterCapacityLabelFn,
|
||||
hiddenOptions,
|
||||
} = props;
|
||||
const [alertElement, setAlertElement] = useState<JSX.Element | undefined>();
|
||||
|
||||
|
@ -419,7 +426,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
maxTasksMenuHeader={maxTasksMenuHeader}
|
||||
enginesLabelFn={enginesLabelFn}
|
||||
maxTasksLabelFn={maxTasksLabelFn}
|
||||
maxTasksOptions={maxTasksOptions}
|
||||
fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
|
||||
hiddenOptions={hiddenOptions}
|
||||
/>
|
||||
{executionState.isLoading() && (
|
||||
<ExecutionTimerPanel
|
||||
|
|
|
@ -99,11 +99,6 @@ const SQL_JOIN_ALGORITHM_LABEL: Record<SqlJoinAlgorithm, string> = {
|
|||
sortMerge: 'Sort merge',
|
||||
};
|
||||
|
||||
const SELECT_DESTINATION_LABEL: Record<SelectDestination, string> = {
|
||||
taskReport: 'Task report',
|
||||
durableStorage: 'Durable storage',
|
||||
};
|
||||
|
||||
const DEFAULT_ENGINES_LABEL_FN = (engine: DruidEngine | undefined) => {
|
||||
switch (engine) {
|
||||
case 'native':
|
||||
|
@ -120,10 +115,33 @@ const DEFAULT_ENGINES_LABEL_FN = (engine: DruidEngine | undefined) => {
|
|||
}
|
||||
};
|
||||
|
||||
const SELECT_DESTINATION_LABEL: Record<SelectDestination, string> = {
|
||||
taskReport: 'Task report',
|
||||
durableStorage: 'Durable storage',
|
||||
};
|
||||
|
||||
const EXPERIMENTAL_ICON = <Icon icon={IconNames.WARNING_SIGN} title="Experimental" />;
|
||||
|
||||
type EnginesMenuOption =
|
||||
| 'edit-query-context'
|
||||
| 'define-parameters'
|
||||
| 'timezone'
|
||||
| 'insert-replace-specific-context'
|
||||
| 'max-parse-exceptions'
|
||||
| 'join-algorithm'
|
||||
| 'select-destination'
|
||||
| 'approximate-count-distinct'
|
||||
| 'finalize-aggregations'
|
||||
| 'group-by-enable-multi-value-unnesting'
|
||||
| 'durable-shuffle-storage'
|
||||
| 'use-cache'
|
||||
| 'approximate-top-n'
|
||||
| 'limit-inline-results';
|
||||
export interface RunPanelProps
|
||||
extends Pick<MaxTasksButtonProps, 'maxTasksLabelFn' | 'fullClusterCapacityLabelFn'> {
|
||||
extends Pick<
|
||||
MaxTasksButtonProps,
|
||||
'maxTasksLabelFn' | 'fullClusterCapacityLabelFn' | 'maxTasksOptions'
|
||||
> {
|
||||
query: WorkbenchQuery;
|
||||
onQueryChange(query: WorkbenchQuery): void;
|
||||
running: boolean;
|
||||
|
@ -134,6 +152,7 @@ export interface RunPanelProps
|
|||
moreMenu?: JSX.Element;
|
||||
maxTasksMenuHeader?: JSX.Element;
|
||||
enginesLabelFn?: (engine: DruidEngine | undefined) => { text: string; label?: string };
|
||||
hiddenOptions?: EnginesMenuOption[];
|
||||
}
|
||||
|
||||
export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
||||
|
@ -149,7 +168,9 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
|||
maxTasksMenuHeader,
|
||||
enginesLabelFn = DEFAULT_ENGINES_LABEL_FN,
|
||||
maxTasksLabelFn,
|
||||
maxTasksOptions,
|
||||
fullClusterCapacityLabelFn,
|
||||
hiddenOptions = [],
|
||||
} = props;
|
||||
const [editContextDialogOpen, setEditContextDialogOpen] = useState(false);
|
||||
const [editParametersDialogOpen, setEditParametersDialogOpen] = useState(false);
|
||||
|
@ -340,19 +361,25 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
|||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
icon={IconNames.PROPERTIES}
|
||||
text="Edit query context..."
|
||||
onClick={() => setEditContextDialogOpen(true)}
|
||||
label={pluralIfNeeded(numContextKeys, 'key')}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={IconNames.HELP}
|
||||
text="Define parameters..."
|
||||
onClick={() => setEditParametersDialogOpen(true)}
|
||||
label={queryParameters ? pluralIfNeeded(queryParameters.length, 'parameter') : ''}
|
||||
/>
|
||||
{effectiveEngine !== 'native' && (
|
||||
{!hiddenOptions.includes('edit-query-context') && (
|
||||
<MenuItem
|
||||
icon={IconNames.PROPERTIES}
|
||||
text="Edit query context..."
|
||||
onClick={() => setEditContextDialogOpen(true)}
|
||||
label={pluralIfNeeded(numContextKeys, 'key')}
|
||||
/>
|
||||
)}
|
||||
{!hiddenOptions.includes('define-parameters') && (
|
||||
<MenuItem
|
||||
icon={IconNames.HELP}
|
||||
text="Define parameters..."
|
||||
onClick={() => setEditParametersDialogOpen(true)}
|
||||
label={
|
||||
queryParameters ? pluralIfNeeded(queryParameters.length, 'parameter') : ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{effectiveEngine !== 'native' && !hiddenOptions.includes('timezone') && (
|
||||
<MenuItem
|
||||
icon={IconNames.GLOBE_NETWORK}
|
||||
text="Timezone"
|
||||
|
@ -393,222 +420,263 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
|||
)}
|
||||
{effectiveEngine === 'sql-msq-task' ? (
|
||||
<>
|
||||
<MenuItem icon={IconNames.BRING_DATA} text="INSERT / REPLACE specific context">
|
||||
<MenuBoolean
|
||||
text="Force segment sort by time"
|
||||
value={forceSegmentSortByTime}
|
||||
onValueChange={forceSegmentSortByTime =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
forceSegmentSortByTime,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
optionsLabelElement={{ false: EXPERIMENTAL_ICON }}
|
||||
/>
|
||||
<MenuBoolean
|
||||
text="Use concurrent locks"
|
||||
value={useConcurrentLocks}
|
||||
onValueChange={useConcurrentLocks =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useConcurrentLocks,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
optionsLabelElement={{ true: EXPERIMENTAL_ICON }}
|
||||
/>
|
||||
<MenuBoolean
|
||||
text="Fail on empty insert"
|
||||
value={failOnEmptyInsert}
|
||||
showUndefined
|
||||
undefinedEffectiveValue={false}
|
||||
onValueChange={failOnEmptyInsert =>
|
||||
changeQueryContext({ ...queryContext, failOnEmptyInsert })
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
<MenuBoolean
|
||||
text="Wait until segments have loaded"
|
||||
value={waitUntilSegmentsLoad}
|
||||
showUndefined
|
||||
undefinedEffectiveValue={ingestMode}
|
||||
onValueChange={waitUntilSegmentsLoad =>
|
||||
changeQueryContext({ ...queryContext, waitUntilSegmentsLoad })
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
{!hiddenOptions.includes('insert-replace-specific-context') && (
|
||||
<MenuItem
|
||||
text="Edit index spec..."
|
||||
label={summarizeIndexSpec(indexSpec)}
|
||||
shouldDismissPopover={false}
|
||||
onClick={() => {
|
||||
setIndexSpecDialogSpec(indexSpec || {});
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={IconNames.ERROR}
|
||||
text="Max parse exceptions"
|
||||
label={String(maxParseExceptions)}
|
||||
>
|
||||
{[0, 1, 5, 10, 1000, 10000, -1].map(v => (
|
||||
<MenuItem
|
||||
key={String(v)}
|
||||
icon={tickIcon(v === maxParseExceptions)}
|
||||
text={v === -1 ? '∞ (-1)' : String(v)}
|
||||
onClick={() =>
|
||||
changeQueryContext({ ...queryContext, maxParseExceptions: v })
|
||||
icon={IconNames.BRING_DATA}
|
||||
text="INSERT / REPLACE specific context"
|
||||
>
|
||||
<MenuBoolean
|
||||
text="Force segment sort by time"
|
||||
value={forceSegmentSortByTime}
|
||||
onValueChange={forceSegmentSortByTime =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
forceSegmentSortByTime,
|
||||
})
|
||||
}
|
||||
shouldDismissPopover={false}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
optionsLabelElement={{ false: EXPERIMENTAL_ICON }}
|
||||
/>
|
||||
))}
|
||||
</MenuItem>
|
||||
<MenuBoolean
|
||||
icon={IconNames.TRANSLATE}
|
||||
text="Finalize aggregations"
|
||||
value={finalizeAggregations}
|
||||
showUndefined
|
||||
undefinedEffectiveValue={!ingestMode}
|
||||
onValueChange={finalizeAggregations =>
|
||||
changeQueryContext({ ...queryContext, finalizeAggregations })
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
<MenuBoolean
|
||||
icon={IconNames.FORK}
|
||||
text="GROUP BY multi-value unnesting"
|
||||
value={groupByEnableMultiValueUnnesting}
|
||||
showUndefined
|
||||
undefinedEffectiveValue={!ingestMode}
|
||||
onValueChange={groupByEnableMultiValueUnnesting =>
|
||||
changeQueryContext({ ...queryContext, groupByEnableMultiValueUnnesting })
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={IconNames.INNER_JOIN}
|
||||
text="Join algorithm"
|
||||
label={
|
||||
SQL_JOIN_ALGORITHM_LABEL[sqlJoinAlgorithm as SqlJoinAlgorithm] ??
|
||||
sqlJoinAlgorithm
|
||||
}
|
||||
>
|
||||
{(['broadcast', 'sortMerge'] as SqlJoinAlgorithm[]).map(o => (
|
||||
<MenuItem
|
||||
key={o}
|
||||
icon={tickIcon(sqlJoinAlgorithm === o)}
|
||||
text={SQL_JOIN_ALGORITHM_LABEL[o]}
|
||||
shouldDismissPopover={false}
|
||||
onClick={() =>
|
||||
changeQueryContext({ ...queryContext, sqlJoinAlgorithm: o })
|
||||
<MenuBoolean
|
||||
text="Use concurrent locks"
|
||||
value={useConcurrentLocks}
|
||||
onValueChange={useConcurrentLocks =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useConcurrentLocks,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
optionsLabelElement={{ true: EXPERIMENTAL_ICON }}
|
||||
/>
|
||||
))}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={IconNames.MANUALLY_ENTERED_DATA}
|
||||
text="SELECT destination"
|
||||
label={
|
||||
SELECT_DESTINATION_LABEL[selectDestination as SelectDestination] ??
|
||||
selectDestination
|
||||
}
|
||||
intent={intent}
|
||||
>
|
||||
{(['taskReport', 'durableStorage'] as SelectDestination[]).map(o => (
|
||||
<MenuItem
|
||||
key={o}
|
||||
icon={tickIcon(selectDestination === o)}
|
||||
text={SELECT_DESTINATION_LABEL[o]}
|
||||
shouldDismissPopover={false}
|
||||
onClick={() =>
|
||||
changeQueryContext({ ...queryContext, selectDestination: o })
|
||||
<MenuBoolean
|
||||
text="Fail on empty insert"
|
||||
value={failOnEmptyInsert}
|
||||
showUndefined
|
||||
undefinedEffectiveValue={false}
|
||||
onValueChange={failOnEmptyInsert =>
|
||||
changeQueryContext({ ...queryContext, failOnEmptyInsert })
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
))}
|
||||
<MenuDivider />
|
||||
<MenuCheckbox
|
||||
checked={selectDestination === 'taskReport' ? !query.unlimited : false}
|
||||
intent={intent}
|
||||
disabled={selectDestination !== 'taskReport'}
|
||||
text="Limit SELECT results in taskReport"
|
||||
labelElement={
|
||||
query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined
|
||||
<MenuBoolean
|
||||
text="Wait until segments have loaded"
|
||||
value={waitUntilSegmentsLoad}
|
||||
showUndefined
|
||||
undefinedEffectiveValue={ingestMode}
|
||||
onValueChange={waitUntilSegmentsLoad =>
|
||||
changeQueryContext({ ...queryContext, waitUntilSegmentsLoad })
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Edit index spec..."
|
||||
label={summarizeIndexSpec(indexSpec)}
|
||||
shouldDismissPopover={false}
|
||||
onClick={() => {
|
||||
setIndexSpecDialogSpec(indexSpec || {});
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!hiddenOptions.includes('max-parse-exceptions') && (
|
||||
<MenuItem
|
||||
icon={IconNames.ERROR}
|
||||
text="Max parse exceptions"
|
||||
label={String(maxParseExceptions)}
|
||||
>
|
||||
{[0, 1, 5, 10, 1000, 10000, -1].map(v => (
|
||||
<MenuItem
|
||||
key={String(v)}
|
||||
icon={tickIcon(v === maxParseExceptions)}
|
||||
text={v === -1 ? '∞ (-1)' : String(v)}
|
||||
onClick={() =>
|
||||
changeQueryContext({ ...queryContext, maxParseExceptions: v })
|
||||
}
|
||||
shouldDismissPopover={false}
|
||||
/>
|
||||
))}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!hiddenOptions.includes('join-algorithm') && (
|
||||
<MenuItem
|
||||
icon={IconNames.INNER_JOIN}
|
||||
text="Join algorithm"
|
||||
label={
|
||||
SQL_JOIN_ALGORITHM_LABEL[sqlJoinAlgorithm as SqlJoinAlgorithm] ??
|
||||
sqlJoinAlgorithm
|
||||
}
|
||||
onChange={() => {
|
||||
onQueryChange(query.toggleUnlimited());
|
||||
}}
|
||||
>
|
||||
{(['broadcast', 'sortMerge'] as SqlJoinAlgorithm[]).map(o => (
|
||||
<MenuItem
|
||||
key={o}
|
||||
icon={tickIcon(sqlJoinAlgorithm === o)}
|
||||
text={SQL_JOIN_ALGORITHM_LABEL[o]}
|
||||
shouldDismissPopover={false}
|
||||
onClick={() =>
|
||||
changeQueryContext({ ...queryContext, sqlJoinAlgorithm: o })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{!hiddenOptions.includes('select-destination') && (
|
||||
<MenuItem
|
||||
icon={IconNames.MANUALLY_ENTERED_DATA}
|
||||
text="SELECT destination"
|
||||
label={
|
||||
SELECT_DESTINATION_LABEL[selectDestination as SelectDestination] ??
|
||||
selectDestination
|
||||
}
|
||||
intent={intent}
|
||||
>
|
||||
{(['taskReport', 'durableStorage'] as SelectDestination[]).map(o => (
|
||||
<MenuItem
|
||||
key={o}
|
||||
icon={tickIcon(selectDestination === o)}
|
||||
text={SELECT_DESTINATION_LABEL[o]}
|
||||
shouldDismissPopover={false}
|
||||
onClick={() =>
|
||||
changeQueryContext({ ...queryContext, selectDestination: o })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<MenuDivider />
|
||||
<MenuCheckbox
|
||||
checked={selectDestination === 'taskReport' ? !query.unlimited : false}
|
||||
intent={intent}
|
||||
disabled={selectDestination !== 'taskReport'}
|
||||
text="Limit SELECT results in taskReport"
|
||||
labelElement={
|
||||
query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined
|
||||
}
|
||||
onChange={() => {
|
||||
onQueryChange(query.toggleUnlimited());
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{!hiddenOptions.includes('approximate-count-distinct') && (
|
||||
<MenuBoolean
|
||||
icon={IconNames.ROCKET_SLANT}
|
||||
text="Approximate COUNT(DISTINCT)"
|
||||
value={useApproximateCountDistinct}
|
||||
onValueChange={useApproximateCountDistinct =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useApproximateCountDistinct,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuBoolean
|
||||
icon={IconNames.CLOUD_TICK}
|
||||
text="Durable shuffle storage"
|
||||
value={durableShuffleStorage}
|
||||
onValueChange={durableShuffleStorage =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
durableShuffleStorage,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hiddenOptions.includes('finalize-aggregations') && (
|
||||
<MenuBoolean
|
||||
icon={IconNames.TRANSLATE}
|
||||
text="Finalize aggregations"
|
||||
value={finalizeAggregations}
|
||||
showUndefined
|
||||
undefinedEffectiveValue={!ingestMode}
|
||||
onValueChange={finalizeAggregations =>
|
||||
changeQueryContext({ ...queryContext, finalizeAggregations })
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
)}
|
||||
{!hiddenOptions.includes('group-by-enable-multi-value-unnesting') && (
|
||||
<MenuBoolean
|
||||
icon={IconNames.FORK}
|
||||
text="GROUP BY multi-value unnesting"
|
||||
value={groupByEnableMultiValueUnnesting}
|
||||
showUndefined
|
||||
undefinedEffectiveValue={!ingestMode}
|
||||
onValueChange={groupByEnableMultiValueUnnesting =>
|
||||
changeQueryContext({ ...queryContext, groupByEnableMultiValueUnnesting })
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
)}
|
||||
{!hiddenOptions.includes('durable-shuffle-storage') && (
|
||||
<MenuBoolean
|
||||
icon={IconNames.CLOUD_TICK}
|
||||
text="Durable shuffle storage"
|
||||
value={durableShuffleStorage}
|
||||
onValueChange={durableShuffleStorage =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
durableShuffleStorage,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuBoolean
|
||||
icon={IconNames.DATA_CONNECTION}
|
||||
text="Use cache"
|
||||
value={useCache}
|
||||
onValueChange={useCache =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useCache,
|
||||
populateCache: useCache,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
<MenuBoolean
|
||||
icon={IconNames.HORIZONTAL_BAR_CHART_DESC}
|
||||
text="Approximate TopN"
|
||||
value={useApproximateTopN}
|
||||
onValueChange={useApproximateTopN =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useApproximateTopN,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
{!hiddenOptions.includes('use-cache') && (
|
||||
<MenuBoolean
|
||||
icon={IconNames.DATA_CONNECTION}
|
||||
text="Use cache"
|
||||
value={useCache}
|
||||
onValueChange={useCache =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useCache,
|
||||
populateCache: useCache,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
)}
|
||||
{!hiddenOptions.includes('approximate-top-n') && (
|
||||
<MenuBoolean
|
||||
icon={IconNames.HORIZONTAL_BAR_CHART_DESC}
|
||||
text="Approximate TopN"
|
||||
value={useApproximateTopN}
|
||||
onValueChange={useApproximateTopN =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useApproximateTopN,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{effectiveEngine !== 'native' && (
|
||||
<MenuBoolean
|
||||
icon={IconNames.ROCKET_SLANT}
|
||||
text="Approximate COUNT(DISTINCT)"
|
||||
value={useApproximateCountDistinct}
|
||||
onValueChange={useApproximateCountDistinct =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useApproximateCountDistinct,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
)}
|
||||
{effectiveEngine === 'sql-native' && (
|
||||
<MenuCheckbox
|
||||
checked={!query.unlimited}
|
||||
intent={query.unlimited ? Intent.WARNING : undefined}
|
||||
text="Limit inline results"
|
||||
labelElement={
|
||||
query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined
|
||||
}
|
||||
onChange={() => {
|
||||
onQueryChange(query.toggleUnlimited());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{effectiveEngine !== 'native' &&
|
||||
effectiveEngine !== 'sql-msq-task' &&
|
||||
!hiddenOptions.includes('approximate-count-distinct') && (
|
||||
<MenuBoolean
|
||||
icon={IconNames.ROCKET_SLANT}
|
||||
text="Approximate COUNT(DISTINCT)"
|
||||
value={useApproximateCountDistinct}
|
||||
onValueChange={useApproximateCountDistinct =>
|
||||
changeQueryContext({
|
||||
...queryContext,
|
||||
useApproximateCountDistinct,
|
||||
})
|
||||
}
|
||||
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
|
||||
/>
|
||||
)}
|
||||
{effectiveEngine === 'sql-native' &&
|
||||
!hiddenOptions.includes('limit-inline-results') && (
|
||||
<MenuCheckbox
|
||||
checked={!query.unlimited}
|
||||
intent={query.unlimited ? Intent.WARNING : undefined}
|
||||
text="Limit inline results"
|
||||
labelElement={
|
||||
query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined
|
||||
}
|
||||
onChange={() => {
|
||||
onQueryChange(query.toggleUnlimited());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
|
@ -630,6 +698,7 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
|||
defaultQueryContext={defaultQueryContext}
|
||||
menuHeader={maxTasksMenuHeader}
|
||||
maxTasksLabelFn={maxTasksLabelFn}
|
||||
maxTasksOptions={maxTasksOptions}
|
||||
fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -96,6 +96,16 @@ function externalDataTabId(tabId: string | undefined): boolean {
|
|||
return String(tabId).startsWith('connect-external-data');
|
||||
}
|
||||
|
||||
type MoreMenuItem =
|
||||
| 'explain'
|
||||
| 'history'
|
||||
| 'prettify'
|
||||
| 'convert-ingestion-to-sql'
|
||||
| 'attach-tab-from-task-id'
|
||||
| 'open-query-detail-archive'
|
||||
| 'druid-sql-documentation'
|
||||
| 'load-demo-queries';
|
||||
|
||||
export interface WorkbenchViewProps
|
||||
extends Pick<
|
||||
QueryTabProps,
|
||||
|
@ -110,10 +120,16 @@ export interface WorkbenchViewProps
|
|||
mandatoryQueryContext?: QueryContext;
|
||||
serverQueryContext?: QueryContext;
|
||||
queryEngines: DruidEngine[];
|
||||
allowExplain: boolean;
|
||||
hiddenMoreMenuItems?: MoreMenuItem[] | ((engine: DruidEngine) => MoreMenuItem[]);
|
||||
goToTask(taskId: string): void;
|
||||
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
|
||||
hideToolbar?: boolean;
|
||||
maxTasksOptions?:
|
||||
| QueryTabProps['maxTasksOptions']
|
||||
| ((engine: DruidEngine) => QueryTabProps['maxTasksOptions']);
|
||||
hiddenOptions?:
|
||||
| QueryTabProps['hiddenOptions']
|
||||
| ((engine: DruidEngine) => QueryTabProps['hiddenOptions']);
|
||||
}
|
||||
|
||||
export interface WorkbenchViewState {
|
||||
|
@ -663,18 +679,24 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
|||
baseQueryContext,
|
||||
serverQueryContext = DEFAULT_SERVER_QUERY_CONTEXT,
|
||||
queryEngines,
|
||||
allowExplain,
|
||||
goToTask,
|
||||
getClusterCapacity,
|
||||
maxTasksMenuHeader,
|
||||
enginesLabelFn,
|
||||
maxTasksLabelFn,
|
||||
maxTasksOptions,
|
||||
fullClusterCapacityLabelFn,
|
||||
hiddenOptions,
|
||||
} = this.props;
|
||||
const { columnMetadataState } = this.state;
|
||||
const currentTabEntry = this.getCurrentTabEntry();
|
||||
const effectiveEngine = currentTabEntry.query.getEffectiveEngine();
|
||||
|
||||
const hiddenMoreMenuItems =
|
||||
typeof this.props.hiddenMoreMenuItems === 'function'
|
||||
? this.props.hiddenMoreMenuItems(effectiveEngine)
|
||||
: this.props.hiddenMoreMenuItems || [];
|
||||
|
||||
return (
|
||||
<div className="center-panel">
|
||||
<div className="tab-and-tool-bar">
|
||||
|
@ -699,10 +721,18 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
|||
maxTasksMenuHeader={maxTasksMenuHeader}
|
||||
enginesLabelFn={enginesLabelFn}
|
||||
maxTasksLabelFn={maxTasksLabelFn}
|
||||
maxTasksOptions={
|
||||
typeof maxTasksOptions === 'function'
|
||||
? maxTasksOptions(effectiveEngine)
|
||||
: maxTasksOptions
|
||||
}
|
||||
fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
|
||||
hiddenOptions={
|
||||
typeof hiddenOptions === 'function' ? hiddenOptions(effectiveEngine) : hiddenOptions
|
||||
}
|
||||
runMoreMenu={
|
||||
<Menu>
|
||||
{allowExplain &&
|
||||
{!hiddenMoreMenuItems.includes('explain') &&
|
||||
(effectiveEngine === 'sql-native' || effectiveEngine === 'sql-msq-task') && (
|
||||
<MenuItem
|
||||
icon={IconNames.CLEAN}
|
||||
|
@ -710,14 +740,14 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
|||
onClick={this.openExplainDialog}
|
||||
/>
|
||||
)}
|
||||
{effectiveEngine !== 'sql-msq-task' && (
|
||||
{effectiveEngine !== 'sql-msq-task' && !hiddenMoreMenuItems.includes('history') && (
|
||||
<MenuItem
|
||||
icon={IconNames.HISTORY}
|
||||
text="Query history"
|
||||
onClick={this.openHistoryDialog}
|
||||
/>
|
||||
)}
|
||||
{currentTabEntry.query.canPrettify() && (
|
||||
{currentTabEntry.query.canPrettify() && !hiddenMoreMenuItems.includes('prettify') && (
|
||||
<MenuItem
|
||||
icon={IconNames.ALIGN_LEFT}
|
||||
text="Prettify query"
|
||||
|
@ -726,38 +756,47 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
|||
)}
|
||||
{queryEngines.includes('sql-msq-task') && (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={IconNames.TEXT_HIGHLIGHT}
|
||||
text="Convert ingestion spec to SQL"
|
||||
onClick={this.openSpecDialog}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={IconNames.DOCUMENT_OPEN}
|
||||
text="Attach tab from task ID"
|
||||
onClick={this.openTaskIdSubmitDialog}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={IconNames.UNARCHIVE}
|
||||
text="Open query detail archive"
|
||||
onClick={this.openExecutionSubmitDialog}
|
||||
/>
|
||||
{!hiddenMoreMenuItems.includes('convert-ingestion-to-sql') && (
|
||||
<MenuItem
|
||||
icon={IconNames.TEXT_HIGHLIGHT}
|
||||
text="Convert ingestion spec to SQL"
|
||||
onClick={this.openSpecDialog}
|
||||
/>
|
||||
)}
|
||||
{!hiddenMoreMenuItems.includes('attach-tab-from-task-id') && (
|
||||
<MenuItem
|
||||
icon={IconNames.DOCUMENT_OPEN}
|
||||
text="Attach tab from task ID"
|
||||
onClick={this.openTaskIdSubmitDialog}
|
||||
/>
|
||||
)}
|
||||
{!hiddenMoreMenuItems.includes('open-query-detail-archive') && (
|
||||
<MenuItem
|
||||
icon={IconNames.UNARCHIVE}
|
||||
text="Open query detail archive"
|
||||
onClick={this.openExecutionSubmitDialog}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={IconNames.HELP}
|
||||
text="DruidSQL documentation"
|
||||
href={getLink('DOCS_SQL')}
|
||||
target="_blank"
|
||||
/>
|
||||
{queryEngines.includes('sql-msq-task') && (
|
||||
{!hiddenMoreMenuItems.includes('druid-sql-documentation') && (
|
||||
<MenuItem
|
||||
icon={IconNames.ROCKET_SLANT}
|
||||
text="Load demo queries"
|
||||
label="(replaces current tabs)"
|
||||
onClick={() => this.handleQueriesChange(getDemoQueries())}
|
||||
icon={IconNames.HELP}
|
||||
text="DruidSQL documentation"
|
||||
href={getLink('DOCS_SQL')}
|
||||
target="_blank"
|
||||
/>
|
||||
)}
|
||||
{queryEngines.includes('sql-msq-task') &&
|
||||
!hiddenMoreMenuItems.includes('load-demo-queries') && (
|
||||
<MenuItem
|
||||
icon={IconNames.ROCKET_SLANT}
|
||||
text="Load demo queries"
|
||||
label="(replaces current tabs)"
|
||||
onClick={() => this.handleQueriesChange(getDemoQueries())}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue