blob: 6a18ee8364898cef3d197e34a5f85c94ff92147e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ButtonProps } from '@blueprintjs/core';
import { Button, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { JSX } from 'react';
import { useState } from 'react';
import { NumericInputDialog } from '../../../dialogs';
import type { QueryContext } from '../../../druid-models';
import { getQueryContextKey } from '../../../druid-models';
import { getLink } from '../../../links';
import { capitalizeFirst, deleteKeys, formatInteger, tickIcon } from '../../../utils';
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)' };
return { text: formatInteger(maxNum), label: `(1 controller + max ${maxNum - 1} workers)` };
};
const DEFAULT_FULL_CLUSTER_CAPACITY_LABEL_FN = (clusterCapacity: number) =>
`${formatInteger(clusterCapacity)} (full cluster capacity)`;
export interface MaxTasksButtonProps extends Omit<ButtonProps, 'text' | 'rightIcon'> {
clusterCapacity: number | undefined;
queryContext: QueryContext;
changeQueryContext(queryContext: QueryContext): void;
defaultQueryContext: QueryContext;
menuHeader?: JSX.Element;
maxTasksLabelFn?: (maxNum: number) => { text: string; label?: string };
maxTasksOptions?: number[];
fullClusterCapacityLabelFn?: (clusterCapacity: number) => string;
}
export const MaxTasksButton = function MaxTasksButton(props: MaxTasksButtonProps) {
const {
clusterCapacity,
queryContext,
changeQueryContext,
defaultQueryContext,
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);
const maxNumTasks = queryContext.maxNumTasks;
const taskAssigment = getQueryContextKey('taskAssignment', queryContext, defaultQueryContext);
const fullClusterCapacity =
typeof clusterCapacity === 'number' ? fullClusterCapacityLabelFn(clusterCapacity) : undefined;
const shownMaxNumTaskOptions = clusterCapacity
? maxTasksOptions.filter(_ => _ <= clusterCapacity)
: maxTasksOptions;
return (
<>
<Popover
className="max-tasks-button"
position={Position.BOTTOM_LEFT}
content={
<Menu>
{menuHeader}
<MenuDivider title="Maximum number of tasks to launch" />
{Boolean(fullClusterCapacity) && (
<MenuItem
icon={tickIcon(typeof maxNumTasks === 'undefined')}
text={fullClusterCapacity}
onClick={() => changeQueryContext(deleteKeys(queryContext, ['maxNumTasks']))}
shouldDismissPopover
/>
)}
{shownMaxNumTaskOptions.map(m => {
const { text, label } = maxTasksLabelFn(m);
return (
<MenuItem
key={String(m)}
icon={tickIcon(m === maxNumTasks)}
text={text}
label={label}
onClick={() => changeQueryContext({ ...queryContext, maxNumTasks: m })}
shouldDismissPopover
/>
);
})}
<MenuItem
icon={tickIcon(
typeof maxNumTasks === 'number' && !shownMaxNumTaskOptions.includes(maxNumTasks),
)}
text="Custom"
onClick={() => setCustomMaxNumTasksDialogOpen(true)}
/>
<MenuDivider />
<MenuItem
icon={IconNames.FLOW_BRANCH}
text="Task assignment"
label={capitalizeFirst(taskAssigment)}
submenuProps={{ style: { width: 300 } }}
>
<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>
}
>
<Button
{...rest}
text={`Max tasks: ${
typeof maxNumTasks === 'undefined'
? clusterCapacity
? fullClusterCapacity
: 2
: formatInteger(maxNumTasks)
}`}
rightIcon={IconNames.CARET_DOWN}
/>
</Popover>
{customMaxNumTasksDialogOpen && (
<NumericInputDialog
title="Custom max task number"
message={
<>
<p>Specify the total number of tasks that should be launched.</p>
<p>
There must be at least 2 tasks to account for the controller and at least one
worker.
</p>
</>
}
minValue={2}
integer
initValue={maxNumTasks || 2}
onSubmit={maxNumTasks => {
changeQueryContext({ ...queryContext, maxNumTasks });
}}
onClose={() => setCustomMaxNumTasksDialogOpen(false)}
/>
)}
</>
);
};