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:
Sébastien 2024-09-10 20:34:49 +02:00 committed by GitHub
parent 2427972c10
commit 5de84253d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 429 additions and 305 deletions

View File

@ -327,7 +327,6 @@ export class ConsoleApplication extends React.PureComponent<
baseQueryContext={baseQueryContext} baseQueryContext={baseQueryContext}
serverQueryContext={serverQueryContext} serverQueryContext={serverQueryContext}
queryEngines={queryEngines} queryEngines={queryEngines}
allowExplain
goToTask={this.goToTasksWithTaskId} goToTask={this.goToTasksWithTaskId}
getClusterCapacity={maybeGetClusterCapacity} getClusterCapacity={maybeGetClusterCapacity}
/>, />,

View File

@ -100,14 +100,13 @@ exports[`MaxTasksButton matches snapshot 1`] = `
multiline={true} multiline={true}
onClick={[Function]} onClick={[Function]}
popoverProps={{}} popoverProps={{}}
shouldDismissPopover={false} shouldDismissPopover={true}
text={ text={
<React.Fragment> <React.Fragment>
<strong> <strong>
Max Max
</strong> </strong>
: : uses the maximum possible tasks up to the specified limit.
uses the maximum possible tasks up to the specified limit.
</React.Fragment> </React.Fragment>
} }
/> />
@ -115,24 +114,28 @@ exports[`MaxTasksButton matches snapshot 1`] = `
active={false} active={false}
disabled={false} disabled={false}
icon="blank" icon="blank"
labelElement={
<Blueprint5.Button
icon="help"
minimal={true}
onClick={[Function]}
/>
}
multiline={true} multiline={true}
onClick={[Function]} onClick={[Function]}
popoverProps={{}} popoverProps={{}}
shouldDismissPopover={false} shouldDismissPopover={true}
text={ text={
<React.Fragment> <React.Fragment>
<strong> <strong>
Auto Auto
</strong> </strong>
: : uses the minimum number of tasks while
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.
<span
onClick={[Function]}
style={
{
"color": "#3eadf9",
"cursor": "pointer",
}
}
>
staying within constraints.
</span>
</React.Fragment> </React.Fragment>
} }
/> />

View File

@ -19,34 +19,17 @@
import type { ButtonProps } from '@blueprintjs/core'; import type { ButtonProps } from '@blueprintjs/core';
import { Button, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core'; import { Button, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import type { JSX, ReactNode } from 'react'; import type { JSX } from 'react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { NumericInputDialog } from '../../../dialogs'; import { NumericInputDialog } from '../../../dialogs';
import type { QueryContext, TaskAssignment } from '../../../druid-models'; import type { QueryContext } from '../../../druid-models';
import { getQueryContextKey } from '../../../druid-models'; import { getQueryContextKey } from '../../../druid-models';
import { getLink } from '../../../links'; import { getLink } from '../../../links';
import { capitalizeFirst, deleteKeys, formatInteger, tickIcon } from '../../../utils'; import { capitalizeFirst, deleteKeys, formatInteger, tickIcon } from '../../../utils';
const MAX_NUM_TASK_OPTIONS = [2, 3, 4, 5, 7, 9, 11, 17, 33, 65, 129]; const DEFAULT_MAX_TASKS_OPTIONS = [2, 3, 4, 5, 7, 9, 11, 17, 33, 65, 129];
const TASK_ASSIGNMENT_OPTIONS: TaskAssignment[] = ['max', 'auto']; const TASK_DOCUMENTATION_LINK = `${getLink('DOCS')}/multi-stage-query/reference#context-parameters`;
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_NUM_TASKS_LABEL_FN = (maxNum: number) => { const DEFAULT_MAX_NUM_TASKS_LABEL_FN = (maxNum: number) => {
if (maxNum === 2) return { text: formatInteger(maxNum), label: '(1 controller + 1 worker)' }; 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; defaultQueryContext: QueryContext;
menuHeader?: JSX.Element; menuHeader?: JSX.Element;
maxTasksLabelFn?: (maxNum: number) => { text: string; label?: string }; maxTasksLabelFn?: (maxNum: number) => { text: string; label?: string };
maxTasksOptions?: number[];
fullClusterCapacityLabelFn?: (clusterCapacity: number) => string; fullClusterCapacityLabelFn?: (clusterCapacity: number) => string;
} }
@ -75,6 +59,7 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
menuHeader, menuHeader,
maxTasksLabelFn = DEFAULT_MAX_NUM_TASKS_LABEL_FN, maxTasksLabelFn = DEFAULT_MAX_NUM_TASKS_LABEL_FN,
fullClusterCapacityLabelFn = DEFAULT_FULL_CLUSTER_CAPACITY_LABEL_FN, fullClusterCapacityLabelFn = DEFAULT_FULL_CLUSTER_CAPACITY_LABEL_FN,
maxTasksOptions = DEFAULT_MAX_TASKS_OPTIONS,
...rest ...rest
} = props; } = props;
const [customMaxNumTasksDialogOpen, setCustomMaxNumTasksDialogOpen] = useState(false); const [customMaxNumTasksDialogOpen, setCustomMaxNumTasksDialogOpen] = useState(false);
@ -86,8 +71,8 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
typeof clusterCapacity === 'number' ? fullClusterCapacityLabelFn(clusterCapacity) : undefined; typeof clusterCapacity === 'number' ? fullClusterCapacityLabelFn(clusterCapacity) : undefined;
const shownMaxNumTaskOptions = clusterCapacity const shownMaxNumTaskOptions = clusterCapacity
? MAX_NUM_TASK_OPTIONS.filter(_ => _ <= clusterCapacity) ? maxTasksOptions.filter(_ => _ <= clusterCapacity)
: MAX_NUM_TASK_OPTIONS; : maxTasksOptions;
return ( return (
<> <>
@ -103,6 +88,7 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
icon={tickIcon(typeof maxNumTasks === 'undefined')} icon={tickIcon(typeof maxNumTasks === 'undefined')}
text={fullClusterCapacity} text={fullClusterCapacity}
onClick={() => changeQueryContext(deleteKeys(queryContext, ['maxNumTasks']))} onClick={() => changeQueryContext(deleteKeys(queryContext, ['maxNumTasks']))}
shouldDismissPopover
/> />
)} )}
{shownMaxNumTaskOptions.map(m => { {shownMaxNumTaskOptions.map(m => {
@ -115,6 +101,7 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
text={text} text={text}
label={label} label={label}
onClick={() => changeQueryContext({ ...queryContext, maxNumTasks: m })} onClick={() => changeQueryContext({ ...queryContext, maxNumTasks: m })}
shouldDismissPopover
/> />
); );
})} })}
@ -132,21 +119,39 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
label={capitalizeFirst(taskAssigment)} label={capitalizeFirst(taskAssigment)}
submenuProps={{ style: { width: 300 } }} submenuProps={{ style: { width: 300 } }}
> >
{TASK_ASSIGNMENT_OPTIONS.map(t => ( <MenuItem
<MenuItem icon={tickIcon(taskAssigment === 'max')}
key={String(t)} text={
icon={tickIcon(t === taskAssigment)} <>
text={ <strong>Max</strong>: uses the maximum possible tasks up to the specified limit.
<> </>
<strong>{capitalizeFirst(t)}</strong>: {TASK_ASSIGNMENT_DESCRIPTION[t]} }
</> multiline
} onClick={() => changeQueryContext({ ...queryContext, taskAssignment: 'max' })}
labelElement={TASK_ASSIGNMENT_LABEL_ELEMENT[t]} />
shouldDismissPopover={false}
multiline <MenuItem
onClick={() => changeQueryContext({ ...queryContext, taskAssignment: t })} 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> </MenuItem>
</Menu> </Menu>
} }

View File

@ -73,7 +73,12 @@ const queryRunner = new QueryRunner({
export interface QueryTabProps export interface QueryTabProps
extends Pick< extends Pick<
RunPanelProps, RunPanelProps,
'maxTasksMenuHeader' | 'enginesLabelFn' | 'maxTasksLabelFn' | 'fullClusterCapacityLabelFn' | 'maxTasksMenuHeader'
| 'enginesLabelFn'
| 'maxTasksLabelFn'
| 'fullClusterCapacityLabelFn'
| 'maxTasksOptions'
| 'hiddenOptions'
> { > {
query: WorkbenchQuery; query: WorkbenchQuery;
id: string; id: string;
@ -110,7 +115,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
maxTasksMenuHeader, maxTasksMenuHeader,
enginesLabelFn, enginesLabelFn,
maxTasksLabelFn, maxTasksLabelFn,
maxTasksOptions,
fullClusterCapacityLabelFn, fullClusterCapacityLabelFn,
hiddenOptions,
} = props; } = props;
const [alertElement, setAlertElement] = useState<JSX.Element | undefined>(); const [alertElement, setAlertElement] = useState<JSX.Element | undefined>();
@ -419,7 +426,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
maxTasksMenuHeader={maxTasksMenuHeader} maxTasksMenuHeader={maxTasksMenuHeader}
enginesLabelFn={enginesLabelFn} enginesLabelFn={enginesLabelFn}
maxTasksLabelFn={maxTasksLabelFn} maxTasksLabelFn={maxTasksLabelFn}
maxTasksOptions={maxTasksOptions}
fullClusterCapacityLabelFn={fullClusterCapacityLabelFn} fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
hiddenOptions={hiddenOptions}
/> />
{executionState.isLoading() && ( {executionState.isLoading() && (
<ExecutionTimerPanel <ExecutionTimerPanel

View File

@ -99,11 +99,6 @@ const SQL_JOIN_ALGORITHM_LABEL: Record<SqlJoinAlgorithm, string> = {
sortMerge: 'Sort merge', sortMerge: 'Sort merge',
}; };
const SELECT_DESTINATION_LABEL: Record<SelectDestination, string> = {
taskReport: 'Task report',
durableStorage: 'Durable storage',
};
const DEFAULT_ENGINES_LABEL_FN = (engine: DruidEngine | undefined) => { const DEFAULT_ENGINES_LABEL_FN = (engine: DruidEngine | undefined) => {
switch (engine) { switch (engine) {
case 'native': 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" />; 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 export interface RunPanelProps
extends Pick<MaxTasksButtonProps, 'maxTasksLabelFn' | 'fullClusterCapacityLabelFn'> { extends Pick<
MaxTasksButtonProps,
'maxTasksLabelFn' | 'fullClusterCapacityLabelFn' | 'maxTasksOptions'
> {
query: WorkbenchQuery; query: WorkbenchQuery;
onQueryChange(query: WorkbenchQuery): void; onQueryChange(query: WorkbenchQuery): void;
running: boolean; running: boolean;
@ -134,6 +152,7 @@ export interface RunPanelProps
moreMenu?: JSX.Element; moreMenu?: JSX.Element;
maxTasksMenuHeader?: JSX.Element; maxTasksMenuHeader?: JSX.Element;
enginesLabelFn?: (engine: DruidEngine | undefined) => { text: string; label?: string }; enginesLabelFn?: (engine: DruidEngine | undefined) => { text: string; label?: string };
hiddenOptions?: EnginesMenuOption[];
} }
export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) { export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
@ -149,7 +168,9 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
maxTasksMenuHeader, maxTasksMenuHeader,
enginesLabelFn = DEFAULT_ENGINES_LABEL_FN, enginesLabelFn = DEFAULT_ENGINES_LABEL_FN,
maxTasksLabelFn, maxTasksLabelFn,
maxTasksOptions,
fullClusterCapacityLabelFn, fullClusterCapacityLabelFn,
hiddenOptions = [],
} = props; } = props;
const [editContextDialogOpen, setEditContextDialogOpen] = useState(false); const [editContextDialogOpen, setEditContextDialogOpen] = useState(false);
const [editParametersDialogOpen, setEditParametersDialogOpen] = useState(false); const [editParametersDialogOpen, setEditParametersDialogOpen] = useState(false);
@ -340,19 +361,25 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
<MenuDivider /> <MenuDivider />
</> </>
)} )}
<MenuItem {!hiddenOptions.includes('edit-query-context') && (
icon={IconNames.PROPERTIES} <MenuItem
text="Edit query context..." icon={IconNames.PROPERTIES}
onClick={() => setEditContextDialogOpen(true)} text="Edit query context..."
label={pluralIfNeeded(numContextKeys, 'key')} onClick={() => setEditContextDialogOpen(true)}
/> label={pluralIfNeeded(numContextKeys, 'key')}
<MenuItem />
icon={IconNames.HELP} )}
text="Define parameters..." {!hiddenOptions.includes('define-parameters') && (
onClick={() => setEditParametersDialogOpen(true)} <MenuItem
label={queryParameters ? pluralIfNeeded(queryParameters.length, 'parameter') : ''} icon={IconNames.HELP}
/> text="Define parameters..."
{effectiveEngine !== 'native' && ( onClick={() => setEditParametersDialogOpen(true)}
label={
queryParameters ? pluralIfNeeded(queryParameters.length, 'parameter') : ''
}
/>
)}
{effectiveEngine !== 'native' && !hiddenOptions.includes('timezone') && (
<MenuItem <MenuItem
icon={IconNames.GLOBE_NETWORK} icon={IconNames.GLOBE_NETWORK}
text="Timezone" text="Timezone"
@ -393,222 +420,263 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
)} )}
{effectiveEngine === 'sql-msq-task' ? ( {effectiveEngine === 'sql-msq-task' ? (
<> <>
<MenuItem icon={IconNames.BRING_DATA} text="INSERT / REPLACE specific context"> {!hiddenOptions.includes('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}
/>
<MenuItem <MenuItem
text="Edit index spec..." icon={IconNames.BRING_DATA}
label={summarizeIndexSpec(indexSpec)} text="INSERT / REPLACE specific context"
shouldDismissPopover={false} >
onClick={() => { <MenuBoolean
setIndexSpecDialogSpec(indexSpec || {}); text="Force segment sort by time"
}} value={forceSegmentSortByTime}
/> onValueChange={forceSegmentSortByTime =>
</MenuItem> changeQueryContext({
<MenuItem ...queryContext,
icon={IconNames.ERROR} forceSegmentSortByTime,
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} optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
optionsLabelElement={{ false: EXPERIMENTAL_ICON }}
/> />
))} <MenuBoolean
</MenuItem> text="Use concurrent locks"
<MenuBoolean value={useConcurrentLocks}
icon={IconNames.TRANSLATE} onValueChange={useConcurrentLocks =>
text="Finalize aggregations" changeQueryContext({
value={finalizeAggregations} ...queryContext,
showUndefined useConcurrentLocks,
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 })
} }
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
optionsLabelElement={{ true: EXPERIMENTAL_ICON }}
/> />
))} <MenuBoolean
</MenuItem> text="Fail on empty insert"
<MenuItem value={failOnEmptyInsert}
icon={IconNames.MANUALLY_ENTERED_DATA} showUndefined
text="SELECT destination" undefinedEffectiveValue={false}
label={ onValueChange={failOnEmptyInsert =>
SELECT_DESTINATION_LABEL[selectDestination as SelectDestination] ?? changeQueryContext({ ...queryContext, failOnEmptyInsert })
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 })
} }
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
/> />
))} <MenuBoolean
<MenuDivider /> text="Wait until segments have loaded"
<MenuCheckbox value={waitUntilSegmentsLoad}
checked={selectDestination === 'taskReport' ? !query.unlimited : false} showUndefined
intent={intent} undefinedEffectiveValue={ingestMode}
disabled={selectDestination !== 'taskReport'} onValueChange={waitUntilSegmentsLoad =>
text="Limit SELECT results in taskReport" changeQueryContext({ ...queryContext, waitUntilSegmentsLoad })
labelElement={ }
query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined 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} {!hiddenOptions.includes('finalize-aggregations') && (
text="Durable shuffle storage" <MenuBoolean
value={durableShuffleStorage} icon={IconNames.TRANSLATE}
onValueChange={durableShuffleStorage => text="Finalize aggregations"
changeQueryContext({ value={finalizeAggregations}
...queryContext, showUndefined
durableShuffleStorage, undefinedEffectiveValue={!ingestMode}
}) onValueChange={finalizeAggregations =>
} changeQueryContext({ ...queryContext, finalizeAggregations })
optionsText={ENABLE_DISABLE_OPTIONS_TEXT} }
/> 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 {!hiddenOptions.includes('use-cache') && (
icon={IconNames.DATA_CONNECTION} <MenuBoolean
text="Use cache" icon={IconNames.DATA_CONNECTION}
value={useCache} text="Use cache"
onValueChange={useCache => value={useCache}
changeQueryContext({ onValueChange={useCache =>
...queryContext, changeQueryContext({
useCache, ...queryContext,
populateCache: useCache, useCache,
}) populateCache: useCache,
} })
optionsText={ENABLE_DISABLE_OPTIONS_TEXT} }
/> optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
<MenuBoolean />
icon={IconNames.HORIZONTAL_BAR_CHART_DESC} )}
text="Approximate TopN" {!hiddenOptions.includes('approximate-top-n') && (
value={useApproximateTopN} <MenuBoolean
onValueChange={useApproximateTopN => icon={IconNames.HORIZONTAL_BAR_CHART_DESC}
changeQueryContext({ text="Approximate TopN"
...queryContext, value={useApproximateTopN}
useApproximateTopN, onValueChange={useApproximateTopN =>
}) changeQueryContext({
} ...queryContext,
optionsText={ENABLE_DISABLE_OPTIONS_TEXT} useApproximateTopN,
/> })
}
optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
/>
)}
</> </>
)} )}
{effectiveEngine !== 'native' && ( {effectiveEngine !== 'native' &&
<MenuBoolean effectiveEngine !== 'sql-msq-task' &&
icon={IconNames.ROCKET_SLANT} !hiddenOptions.includes('approximate-count-distinct') && (
text="Approximate COUNT(DISTINCT)" <MenuBoolean
value={useApproximateCountDistinct} icon={IconNames.ROCKET_SLANT}
onValueChange={useApproximateCountDistinct => text="Approximate COUNT(DISTINCT)"
changeQueryContext({ value={useApproximateCountDistinct}
...queryContext, onValueChange={useApproximateCountDistinct =>
useApproximateCountDistinct, changeQueryContext({
}) ...queryContext,
} useApproximateCountDistinct,
optionsText={ENABLE_DISABLE_OPTIONS_TEXT} })
/> }
)} optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
{effectiveEngine === 'sql-native' && ( />
<MenuCheckbox )}
checked={!query.unlimited} {effectiveEngine === 'sql-native' &&
intent={query.unlimited ? Intent.WARNING : undefined} !hiddenOptions.includes('limit-inline-results') && (
text="Limit inline results" <MenuCheckbox
labelElement={ checked={!query.unlimited}
query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined intent={query.unlimited ? Intent.WARNING : undefined}
} text="Limit inline results"
onChange={() => { labelElement={
onQueryChange(query.toggleUnlimited()); query.unlimited ? <Icon icon={IconNames.WARNING_SIGN} /> : undefined
}} }
/> onChange={() => {
)} onQueryChange(query.toggleUnlimited());
}}
/>
)}
</Menu> </Menu>
} }
> >
@ -630,6 +698,7 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
defaultQueryContext={defaultQueryContext} defaultQueryContext={defaultQueryContext}
menuHeader={maxTasksMenuHeader} menuHeader={maxTasksMenuHeader}
maxTasksLabelFn={maxTasksLabelFn} maxTasksLabelFn={maxTasksLabelFn}
maxTasksOptions={maxTasksOptions}
fullClusterCapacityLabelFn={fullClusterCapacityLabelFn} fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
/> />
)} )}

View File

@ -96,6 +96,16 @@ function externalDataTabId(tabId: string | undefined): boolean {
return String(tabId).startsWith('connect-external-data'); 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 export interface WorkbenchViewProps
extends Pick< extends Pick<
QueryTabProps, QueryTabProps,
@ -110,10 +120,16 @@ export interface WorkbenchViewProps
mandatoryQueryContext?: QueryContext; mandatoryQueryContext?: QueryContext;
serverQueryContext?: QueryContext; serverQueryContext?: QueryContext;
queryEngines: DruidEngine[]; queryEngines: DruidEngine[];
allowExplain: boolean; hiddenMoreMenuItems?: MoreMenuItem[] | ((engine: DruidEngine) => MoreMenuItem[]);
goToTask(taskId: string): void; goToTask(taskId: string): void;
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined; getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
hideToolbar?: boolean; hideToolbar?: boolean;
maxTasksOptions?:
| QueryTabProps['maxTasksOptions']
| ((engine: DruidEngine) => QueryTabProps['maxTasksOptions']);
hiddenOptions?:
| QueryTabProps['hiddenOptions']
| ((engine: DruidEngine) => QueryTabProps['hiddenOptions']);
} }
export interface WorkbenchViewState { export interface WorkbenchViewState {
@ -663,18 +679,24 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
baseQueryContext, baseQueryContext,
serverQueryContext = DEFAULT_SERVER_QUERY_CONTEXT, serverQueryContext = DEFAULT_SERVER_QUERY_CONTEXT,
queryEngines, queryEngines,
allowExplain,
goToTask, goToTask,
getClusterCapacity, getClusterCapacity,
maxTasksMenuHeader, maxTasksMenuHeader,
enginesLabelFn, enginesLabelFn,
maxTasksLabelFn, maxTasksLabelFn,
maxTasksOptions,
fullClusterCapacityLabelFn, fullClusterCapacityLabelFn,
hiddenOptions,
} = this.props; } = this.props;
const { columnMetadataState } = this.state; const { columnMetadataState } = this.state;
const currentTabEntry = this.getCurrentTabEntry(); const currentTabEntry = this.getCurrentTabEntry();
const effectiveEngine = currentTabEntry.query.getEffectiveEngine(); const effectiveEngine = currentTabEntry.query.getEffectiveEngine();
const hiddenMoreMenuItems =
typeof this.props.hiddenMoreMenuItems === 'function'
? this.props.hiddenMoreMenuItems(effectiveEngine)
: this.props.hiddenMoreMenuItems || [];
return ( return (
<div className="center-panel"> <div className="center-panel">
<div className="tab-and-tool-bar"> <div className="tab-and-tool-bar">
@ -699,10 +721,18 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
maxTasksMenuHeader={maxTasksMenuHeader} maxTasksMenuHeader={maxTasksMenuHeader}
enginesLabelFn={enginesLabelFn} enginesLabelFn={enginesLabelFn}
maxTasksLabelFn={maxTasksLabelFn} maxTasksLabelFn={maxTasksLabelFn}
maxTasksOptions={
typeof maxTasksOptions === 'function'
? maxTasksOptions(effectiveEngine)
: maxTasksOptions
}
fullClusterCapacityLabelFn={fullClusterCapacityLabelFn} fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
hiddenOptions={
typeof hiddenOptions === 'function' ? hiddenOptions(effectiveEngine) : hiddenOptions
}
runMoreMenu={ runMoreMenu={
<Menu> <Menu>
{allowExplain && {!hiddenMoreMenuItems.includes('explain') &&
(effectiveEngine === 'sql-native' || effectiveEngine === 'sql-msq-task') && ( (effectiveEngine === 'sql-native' || effectiveEngine === 'sql-msq-task') && (
<MenuItem <MenuItem
icon={IconNames.CLEAN} icon={IconNames.CLEAN}
@ -710,14 +740,14 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
onClick={this.openExplainDialog} onClick={this.openExplainDialog}
/> />
)} )}
{effectiveEngine !== 'sql-msq-task' && ( {effectiveEngine !== 'sql-msq-task' && !hiddenMoreMenuItems.includes('history') && (
<MenuItem <MenuItem
icon={IconNames.HISTORY} icon={IconNames.HISTORY}
text="Query history" text="Query history"
onClick={this.openHistoryDialog} onClick={this.openHistoryDialog}
/> />
)} )}
{currentTabEntry.query.canPrettify() && ( {currentTabEntry.query.canPrettify() && !hiddenMoreMenuItems.includes('prettify') && (
<MenuItem <MenuItem
icon={IconNames.ALIGN_LEFT} icon={IconNames.ALIGN_LEFT}
text="Prettify query" text="Prettify query"
@ -726,38 +756,47 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
)} )}
{queryEngines.includes('sql-msq-task') && ( {queryEngines.includes('sql-msq-task') && (
<> <>
<MenuItem {!hiddenMoreMenuItems.includes('convert-ingestion-to-sql') && (
icon={IconNames.TEXT_HIGHLIGHT} <MenuItem
text="Convert ingestion spec to SQL" icon={IconNames.TEXT_HIGHLIGHT}
onClick={this.openSpecDialog} text="Convert ingestion spec to SQL"
/> onClick={this.openSpecDialog}
<MenuItem />
icon={IconNames.DOCUMENT_OPEN} )}
text="Attach tab from task ID" {!hiddenMoreMenuItems.includes('attach-tab-from-task-id') && (
onClick={this.openTaskIdSubmitDialog} <MenuItem
/> icon={IconNames.DOCUMENT_OPEN}
<MenuItem text="Attach tab from task ID"
icon={IconNames.UNARCHIVE} onClick={this.openTaskIdSubmitDialog}
text="Open query detail archive" />
onClick={this.openExecutionSubmitDialog} )}
/> {!hiddenMoreMenuItems.includes('open-query-detail-archive') && (
<MenuItem
icon={IconNames.UNARCHIVE}
text="Open query detail archive"
onClick={this.openExecutionSubmitDialog}
/>
)}
</> </>
)} )}
<MenuDivider /> <MenuDivider />
<MenuItem {!hiddenMoreMenuItems.includes('druid-sql-documentation') && (
icon={IconNames.HELP}
text="DruidSQL documentation"
href={getLink('DOCS_SQL')}
target="_blank"
/>
{queryEngines.includes('sql-msq-task') && (
<MenuItem <MenuItem
icon={IconNames.ROCKET_SLANT} icon={IconNames.HELP}
text="Load demo queries" text="DruidSQL documentation"
label="(replaces current tabs)" href={getLink('DOCS_SQL')}
onClick={() => this.handleQueriesChange(getDemoQueries())} 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> </Menu>
} }
/> />