mirror of https://github.com/apache/druid.git
Exposes hooks to customize the workbench-view (#16749)
* Exposes hooks to customize the workbench-view * addressed PR feedback * naming * auto -> formatInteger(maxNum)
This commit is contained in:
parent
b1edf4a5b4
commit
e286be9427
|
@ -39,14 +39,28 @@ const TASK_ASSIGNMENT_DESCRIPTION: Record<string, string> = {
|
||||||
auto: `Use as few tasks as possible without exceeding 512 MiB or 10,000 files per task, unless exceeding these limits is necessary to stay within 'maxNumTasks'. When calculating the size of files, the weighted size is used, which considers the file format and compression format used if any. When file sizes cannot be determined through directory listing (for example: http), behaves the same as 'max'.`,
|
auto: `Use as few tasks as possible without exceeding 512 MiB or 10,000 files per task, unless exceeding these limits is necessary to stay within 'maxNumTasks'. When calculating the size of files, the weighted size is used, which considers the file format and compression format used if any. When file sizes cannot be determined through directory listing (for example: http), behaves the same as 'max'.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_NUM_LABEL_FN = (maxNum: number) => {
|
||||||
|
if (maxNum === 2) return { text: formatInteger(maxNum), label: '(1 controller + 1 worker)' };
|
||||||
|
return { text: formatInteger(maxNum), label: `(1 controller + max ${maxNum - 1} workers)` };
|
||||||
|
};
|
||||||
|
|
||||||
export interface MaxTasksButtonProps extends Omit<ButtonProps, 'text' | 'rightIcon'> {
|
export interface MaxTasksButtonProps extends Omit<ButtonProps, 'text' | 'rightIcon'> {
|
||||||
clusterCapacity: number | undefined;
|
clusterCapacity: number | undefined;
|
||||||
queryContext: QueryContext;
|
queryContext: QueryContext;
|
||||||
changeQueryContext(queryContext: QueryContext): void;
|
changeQueryContext(queryContext: QueryContext): void;
|
||||||
|
menuHeader?: JSX.Element;
|
||||||
|
maxNumLabelFn?: (maxNum: number) => { text: string; label?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps) {
|
export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps) {
|
||||||
const { clusterCapacity, queryContext, changeQueryContext, ...rest } = props;
|
const {
|
||||||
|
clusterCapacity,
|
||||||
|
queryContext,
|
||||||
|
changeQueryContext,
|
||||||
|
menuHeader,
|
||||||
|
maxNumLabelFn = DEFAULT_MAX_NUM_LABEL_FN,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
const [customMaxNumTasksDialogOpen, setCustomMaxNumTasksDialogOpen] = useState(false);
|
const [customMaxNumTasksDialogOpen, setCustomMaxNumTasksDialogOpen] = useState(false);
|
||||||
|
|
||||||
const maxNumTasks = getMaxNumTasks(queryContext);
|
const maxNumTasks = getMaxNumTasks(queryContext);
|
||||||
|
@ -68,6 +82,7 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
|
||||||
position={Position.BOTTOM_LEFT}
|
position={Position.BOTTOM_LEFT}
|
||||||
content={
|
content={
|
||||||
<Menu>
|
<Menu>
|
||||||
|
{menuHeader}
|
||||||
<MenuDivider title="Maximum number of tasks to launch" />
|
<MenuDivider title="Maximum number of tasks to launch" />
|
||||||
{Boolean(fullClusterCapacity) && (
|
{Boolean(fullClusterCapacity) && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -76,15 +91,19 @@ export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps
|
||||||
onClick={() => changeQueryContext(changeMaxNumTasks(queryContext, undefined))}
|
onClick={() => changeQueryContext(changeMaxNumTasks(queryContext, undefined))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shownMaxNumTaskOptions.map(m => (
|
{shownMaxNumTaskOptions.map(m => {
|
||||||
<MenuItem
|
const { text, label } = maxNumLabelFn(m);
|
||||||
key={String(m)}
|
|
||||||
icon={tickIcon(m === maxNumTasks)}
|
return (
|
||||||
text={formatInteger(m)}
|
<MenuItem
|
||||||
label={`(1 controller + ${m === 2 ? '1 worker' : `max ${m - 1} workers`})`}
|
key={String(m)}
|
||||||
onClick={() => changeQueryContext(changeMaxNumTasks(queryContext, m))}
|
icon={tickIcon(m === maxNumTasks)}
|
||||||
/>
|
text={text}
|
||||||
))}
|
label={label}
|
||||||
|
onClick={() => changeQueryContext(changeMaxNumTasks(queryContext, m))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={tickIcon(
|
icon={tickIcon(
|
||||||
typeof maxNumTasks === 'number' && !shownMaxNumTaskOptions.includes(maxNumTasks),
|
typeof maxNumTasks === 'number' && !shownMaxNumTaskOptions.includes(maxNumTasks),
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { IconNames } from '@blueprintjs/icons';
|
||||||
import type { QueryResult } from '@druid-toolkit/query';
|
import type { QueryResult } from '@druid-toolkit/query';
|
||||||
import { QueryRunner, SqlQuery } from '@druid-toolkit/query';
|
import { QueryRunner, SqlQuery } from '@druid-toolkit/query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { JSX } from 'react';
|
import type { ComponentProps, JSX } from 'react';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import SplitterLayout from 'react-splitter-layout';
|
import SplitterLayout from 'react-splitter-layout';
|
||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
|
@ -82,6 +82,9 @@ export interface QueryTabProps {
|
||||||
clusterCapacity: number | undefined;
|
clusterCapacity: number | undefined;
|
||||||
goToTask(taskId: string): void;
|
goToTask(taskId: string): void;
|
||||||
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
|
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
|
||||||
|
maxTaskMenuHeader?: JSX.Element;
|
||||||
|
enginesLabelFn?: ComponentProps<typeof RunPanel>['enginesLabelFn'];
|
||||||
|
maxTaskLabelFn?: ComponentProps<typeof RunPanel>['maxTaskLabelFn'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
||||||
|
@ -98,6 +101,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
||||||
clusterCapacity,
|
clusterCapacity,
|
||||||
goToTask,
|
goToTask,
|
||||||
getClusterCapacity,
|
getClusterCapacity,
|
||||||
|
maxTaskMenuHeader,
|
||||||
|
enginesLabelFn,
|
||||||
|
maxTaskLabelFn,
|
||||||
} = props;
|
} = props;
|
||||||
const [alertElement, setAlertElement] = useState<JSX.Element | undefined>();
|
const [alertElement, setAlertElement] = useState<JSX.Element | undefined>();
|
||||||
|
|
||||||
|
@ -399,6 +405,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
||||||
queryEngines={queryEngines}
|
queryEngines={queryEngines}
|
||||||
clusterCapacity={clusterCapacity}
|
clusterCapacity={clusterCapacity}
|
||||||
moreMenu={runMoreMenu}
|
moreMenu={runMoreMenu}
|
||||||
|
maxTaskMenuHeader={maxTaskMenuHeader}
|
||||||
|
enginesLabelFn={enginesLabelFn}
|
||||||
|
maxTaskLabelFn={maxTaskLabelFn}
|
||||||
/>
|
/>
|
||||||
{executionState.isLoading() && (
|
{executionState.isLoading() && (
|
||||||
<ExecutionTimerPanel
|
<ExecutionTimerPanel
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
useHotkeys,
|
useHotkeys,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import type { JSX } from 'react';
|
import type { ComponentProps, JSX } from 'react';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { MenuCheckbox, MenuTristate } from '../../../components';
|
import { MenuCheckbox, MenuTristate } from '../../../components';
|
||||||
|
@ -111,6 +111,14 @@ const ARRAY_INGEST_MODE_DESCRIPTION: Record<ArrayIngestMode, JSX.Element> = {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_ENGINES_LABEL_FN = (engine: DruidEngine | undefined) => {
|
||||||
|
if (!engine) return { text: 'auto' };
|
||||||
|
return {
|
||||||
|
text: engine,
|
||||||
|
label: engine === 'sql-msq-task' ? 'multi-stage-query' : undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export interface RunPanelProps {
|
export interface RunPanelProps {
|
||||||
query: WorkbenchQuery;
|
query: WorkbenchQuery;
|
||||||
onQueryChange(query: WorkbenchQuery): void;
|
onQueryChange(query: WorkbenchQuery): void;
|
||||||
|
@ -120,11 +128,25 @@ export interface RunPanelProps {
|
||||||
queryEngines: DruidEngine[];
|
queryEngines: DruidEngine[];
|
||||||
clusterCapacity: number | undefined;
|
clusterCapacity: number | undefined;
|
||||||
moreMenu?: JSX.Element;
|
moreMenu?: JSX.Element;
|
||||||
|
maxTaskMenuHeader?: JSX.Element;
|
||||||
|
enginesLabelFn?: (engine: DruidEngine | undefined) => { text: string; label?: string };
|
||||||
|
maxTaskLabelFn?: ComponentProps<typeof MaxTasksButton>['maxNumLabelFn'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
||||||
const { query, onQueryChange, onRun, moreMenu, running, small, queryEngines, clusterCapacity } =
|
const {
|
||||||
props;
|
query,
|
||||||
|
onQueryChange,
|
||||||
|
onRun,
|
||||||
|
moreMenu,
|
||||||
|
running,
|
||||||
|
small,
|
||||||
|
queryEngines,
|
||||||
|
clusterCapacity,
|
||||||
|
maxTaskMenuHeader,
|
||||||
|
maxTaskLabelFn,
|
||||||
|
enginesLabelFn = DEFAULT_ENGINES_LABEL_FN,
|
||||||
|
} = props;
|
||||||
const [editContextDialogOpen, setEditContextDialogOpen] = useState(false);
|
const [editContextDialogOpen, setEditContextDialogOpen] = useState(false);
|
||||||
const [editParametersDialogOpen, setEditParametersDialogOpen] = useState(false);
|
const [editParametersDialogOpen, setEditParametersDialogOpen] = useState(false);
|
||||||
const [customTimezoneDialogOpen, setCustomTimezoneDialogOpen] = useState(false);
|
const [customTimezoneDialogOpen, setCustomTimezoneDialogOpen] = useState(false);
|
||||||
|
@ -186,25 +208,11 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
||||||
useHotkeys(hotkeys);
|
useHotkeys(hotkeys);
|
||||||
|
|
||||||
const queryEngine = query.engine;
|
const queryEngine = query.engine;
|
||||||
function renderQueryEngineMenuItem(e: DruidEngine | undefined) {
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
key={String(e)}
|
|
||||||
icon={tickIcon(e === queryEngine)}
|
|
||||||
text={typeof e === 'undefined' ? 'auto' : e}
|
|
||||||
label={e === 'sql-msq-task' ? 'multi-stage-query' : undefined}
|
|
||||||
onClick={() => onQueryChange(query.changeEngine(e))}
|
|
||||||
shouldDismissPopover={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeQueryContext(queryContext: QueryContext) {
|
function changeQueryContext(queryContext: QueryContext) {
|
||||||
onQueryChange(query.changeQueryContext(queryContext));
|
onQueryChange(query.changeQueryContext(queryContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableEngines = ([undefined] as (DruidEngine | undefined)[]).concat(queryEngines);
|
|
||||||
|
|
||||||
function offsetOptions(): JSX.Element[] {
|
function offsetOptions(): JSX.Element[] {
|
||||||
const items: JSX.Element[] = [];
|
const items: JSX.Element[] = [];
|
||||||
|
|
||||||
|
@ -231,6 +239,9 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
||||||
const intent = overloadWarning ? Intent.WARNING : undefined;
|
const intent = overloadWarning ? Intent.WARNING : undefined;
|
||||||
|
|
||||||
const effectiveEngine = query.getEffectiveEngine();
|
const effectiveEngine = query.getEffectiveEngine();
|
||||||
|
|
||||||
|
const autoEngineLabel = enginesLabelFn(undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="run-panel">
|
<div className="run-panel">
|
||||||
<Button
|
<Button
|
||||||
|
@ -262,7 +273,29 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
||||||
{queryEngines.length > 1 && (
|
{queryEngines.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<MenuDivider title="Select engine" />
|
<MenuDivider title="Select engine" />
|
||||||
{availableEngines.map(renderQueryEngineMenuItem)}
|
<MenuItem
|
||||||
|
key="auto"
|
||||||
|
icon={tickIcon(queryEngine === undefined)}
|
||||||
|
text={autoEngineLabel.text}
|
||||||
|
label={autoEngineLabel.label}
|
||||||
|
onClick={() => onQueryChange(query.changeEngine(undefined))}
|
||||||
|
shouldDismissPopover={false}
|
||||||
|
/>
|
||||||
|
{queryEngines.map(engine => {
|
||||||
|
const { text, label } = enginesLabelFn(engine);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={String(engine)}
|
||||||
|
icon={tickIcon(engine === queryEngine)}
|
||||||
|
text={text}
|
||||||
|
label={label}
|
||||||
|
onClick={() => onQueryChange(query.changeEngine(engine))}
|
||||||
|
shouldDismissPopover={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -485,7 +518,10 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
text={`Engine: ${queryEngine || `auto (${effectiveEngine})`}`}
|
text={`Engine: ${
|
||||||
|
(enginesLabelFn ? enginesLabelFn(queryEngine).text : queryEngine) ||
|
||||||
|
`auto (${enginesLabelFn ? enginesLabelFn(effectiveEngine) : effectiveEngine})`
|
||||||
|
}`}
|
||||||
rightIcon={IconNames.CARET_DOWN}
|
rightIcon={IconNames.CARET_DOWN}
|
||||||
intent={intent}
|
intent={intent}
|
||||||
/>
|
/>
|
||||||
|
@ -495,6 +531,8 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
||||||
clusterCapacity={clusterCapacity}
|
clusterCapacity={clusterCapacity}
|
||||||
queryContext={queryContext}
|
queryContext={queryContext}
|
||||||
changeQueryContext={changeQueryContext}
|
changeQueryContext={changeQueryContext}
|
||||||
|
menuHeader={maxTaskMenuHeader}
|
||||||
|
maxNumLabelFn={maxTaskLabelFn}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{ingestMode && (
|
{ingestMode && (
|
||||||
|
|
|
@ -30,7 +30,7 @@ import type { SqlQuery } from '@druid-toolkit/query';
|
||||||
import { SqlExpression } from '@druid-toolkit/query';
|
import { SqlExpression } from '@druid-toolkit/query';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import React from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
|
|
||||||
import { SpecDialog, StringInputDialog } from '../../dialogs';
|
import { SpecDialog, StringInputDialog } from '../../dialogs';
|
||||||
import type {
|
import type {
|
||||||
|
@ -101,6 +101,9 @@ export interface WorkbenchViewProps {
|
||||||
allowExplain: boolean;
|
allowExplain: boolean;
|
||||||
goToTask(taskId: string): void;
|
goToTask(taskId: string): void;
|
||||||
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
|
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
|
||||||
|
maxTaskMenuHeader?: JSX.Element;
|
||||||
|
enginesLabelFn?: ComponentProps<typeof QueryTab>['enginesLabelFn'];
|
||||||
|
maxTaskLabelFn?: ComponentProps<typeof QueryTab>['maxTaskLabelFn'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchViewState {
|
export interface WorkbenchViewState {
|
||||||
|
@ -649,6 +652,9 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
||||||
allowExplain,
|
allowExplain,
|
||||||
goToTask,
|
goToTask,
|
||||||
getClusterCapacity,
|
getClusterCapacity,
|
||||||
|
maxTaskMenuHeader,
|
||||||
|
enginesLabelFn,
|
||||||
|
maxTaskLabelFn,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { columnMetadataState } = this.state;
|
const { columnMetadataState } = this.state;
|
||||||
const currentTabEntry = this.getCurrentTabEntry();
|
const currentTabEntry = this.getCurrentTabEntry();
|
||||||
|
@ -673,6 +679,9 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
||||||
clusterCapacity={capabilities.getMaxTaskSlots()}
|
clusterCapacity={capabilities.getMaxTaskSlots()}
|
||||||
goToTask={goToTask}
|
goToTask={goToTask}
|
||||||
getClusterCapacity={getClusterCapacity}
|
getClusterCapacity={getClusterCapacity}
|
||||||
|
maxTaskMenuHeader={maxTaskMenuHeader}
|
||||||
|
enginesLabelFn={enginesLabelFn}
|
||||||
|
maxTaskLabelFn={maxTaskLabelFn}
|
||||||
runMoreMenu={
|
runMoreMenu={
|
||||||
<Menu>
|
<Menu>
|
||||||
{allowExplain &&
|
{allowExplain &&
|
||||||
|
|
Loading…
Reference in New Issue