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
key={String(t)} icon={tickIcon(taskAssigment === 'max')}
icon={tickIcon(t === taskAssigment)}
text={ text={
<> <>
<strong>{capitalizeFirst(t)}</strong>: {TASK_ASSIGNMENT_DESCRIPTION[t]} <strong>Max</strong>: uses the maximum possible tasks up to the specified limit.
</> </>
} }
labelElement={TASK_ASSIGNMENT_LABEL_ELEMENT[t]}
shouldDismissPopover={false}
multiline multiline
onClick={() => changeQueryContext({ ...queryContext, taskAssignment: t })} 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> </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 />
</> </>
)} )}
{!hiddenOptions.includes('edit-query-context') && (
<MenuItem <MenuItem
icon={IconNames.PROPERTIES} icon={IconNames.PROPERTIES}
text="Edit query context..." text="Edit query context..."
onClick={() => setEditContextDialogOpen(true)} onClick={() => setEditContextDialogOpen(true)}
label={pluralIfNeeded(numContextKeys, 'key')} label={pluralIfNeeded(numContextKeys, 'key')}
/> />
)}
{!hiddenOptions.includes('define-parameters') && (
<MenuItem <MenuItem
icon={IconNames.HELP} icon={IconNames.HELP}
text="Define parameters..." text="Define parameters..."
onClick={() => setEditParametersDialogOpen(true)} onClick={() => setEditParametersDialogOpen(true)}
label={queryParameters ? pluralIfNeeded(queryParameters.length, 'parameter') : ''} label={
queryParameters ? pluralIfNeeded(queryParameters.length, 'parameter') : ''
}
/> />
{effectiveEngine !== 'native' && ( )}
{effectiveEngine !== 'native' && !hiddenOptions.includes('timezone') && (
<MenuItem <MenuItem
icon={IconNames.GLOBE_NETWORK} icon={IconNames.GLOBE_NETWORK}
text="Timezone" text="Timezone"
@ -393,7 +420,11 @@ 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') && (
<MenuItem
icon={IconNames.BRING_DATA}
text="INSERT / REPLACE specific context"
>
<MenuBoolean <MenuBoolean
text="Force segment sort by time" text="Force segment sort by time"
value={forceSegmentSortByTime} value={forceSegmentSortByTime}
@ -447,6 +478,8 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
}} }}
/> />
</MenuItem> </MenuItem>
)}
{!hiddenOptions.includes('max-parse-exceptions') && (
<MenuItem <MenuItem
icon={IconNames.ERROR} icon={IconNames.ERROR}
text="Max parse exceptions" text="Max parse exceptions"
@ -464,28 +497,8 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
/> />
))} ))}
</MenuItem> </MenuItem>
<MenuBoolean )}
icon={IconNames.TRANSLATE} {!hiddenOptions.includes('join-algorithm') && (
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 <MenuItem
icon={IconNames.INNER_JOIN} icon={IconNames.INNER_JOIN}
text="Join algorithm" text="Join algorithm"
@ -506,6 +519,9 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
/> />
))} ))}
</MenuItem> </MenuItem>
)}
{!hiddenOptions.includes('select-destination') && (
<MenuItem <MenuItem
icon={IconNames.MANUALLY_ENTERED_DATA} icon={IconNames.MANUALLY_ENTERED_DATA}
text="SELECT destination" text="SELECT destination"
@ -540,49 +556,9 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
}} }}
/> />
</MenuItem> </MenuItem>
<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}
/>
</>
)} )}
{effectiveEngine !== 'native' && (
{!hiddenOptions.includes('approximate-count-distinct') && (
<MenuBoolean <MenuBoolean
icon={IconNames.ROCKET_SLANT} icon={IconNames.ROCKET_SLANT}
text="Approximate COUNT(DISTINCT)" text="Approximate COUNT(DISTINCT)"
@ -596,7 +572,99 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
optionsText={ENABLE_DISABLE_OPTIONS_TEXT} optionsText={ENABLE_DISABLE_OPTIONS_TEXT}
/> />
)} )}
{effectiveEngine === 'sql-native' && (
{!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}
/>
)}
</>
) : (
<>
{!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' &&
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 <MenuCheckbox
checked={!query.unlimited} checked={!query.unlimited}
intent={query.unlimited ? Intent.WARNING : undefined} intent={query.unlimited ? Intent.WARNING : undefined}
@ -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,31 +756,40 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
)} )}
{queryEngines.includes('sql-msq-task') && ( {queryEngines.includes('sql-msq-task') && (
<> <>
{!hiddenMoreMenuItems.includes('convert-ingestion-to-sql') && (
<MenuItem <MenuItem
icon={IconNames.TEXT_HIGHLIGHT} icon={IconNames.TEXT_HIGHLIGHT}
text="Convert ingestion spec to SQL" text="Convert ingestion spec to SQL"
onClick={this.openSpecDialog} onClick={this.openSpecDialog}
/> />
)}
{!hiddenMoreMenuItems.includes('attach-tab-from-task-id') && (
<MenuItem <MenuItem
icon={IconNames.DOCUMENT_OPEN} icon={IconNames.DOCUMENT_OPEN}
text="Attach tab from task ID" text="Attach tab from task ID"
onClick={this.openTaskIdSubmitDialog} onClick={this.openTaskIdSubmitDialog}
/> />
)}
{!hiddenMoreMenuItems.includes('open-query-detail-archive') && (
<MenuItem <MenuItem
icon={IconNames.UNARCHIVE} icon={IconNames.UNARCHIVE}
text="Open query detail archive" text="Open query detail archive"
onClick={this.openExecutionSubmitDialog} onClick={this.openExecutionSubmitDialog}
/> />
)}
</> </>
)} )}
<MenuDivider /> <MenuDivider />
{!hiddenMoreMenuItems.includes('druid-sql-documentation') && (
<MenuItem <MenuItem
icon={IconNames.HELP} icon={IconNames.HELP}
text="DruidSQL documentation" text="DruidSQL documentation"
href={getLink('DOCS_SQL')} href={getLink('DOCS_SQL')}
target="_blank" target="_blank"
/> />
{queryEngines.includes('sql-msq-task') && ( )}
{queryEngines.includes('sql-msq-task') &&
!hiddenMoreMenuItems.includes('load-demo-queries') && (
<MenuItem <MenuItem
icon={IconNames.ROCKET_SLANT} icon={IconNames.ROCKET_SLANT}
text="Load demo queries" text="Load demo queries"