This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 483a03f26c7 Web console: Server context defaults (#16868)
483a03f26c7 is described below
commit 483a03f26c7221f10493d86715985a58e61af967
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Fri Aug 9 14:46:59 2024 -0700
Web console: Server context defaults (#16868)
* add server defaults
* null is NULL
* r to d
* add test
* typo
---
licenses.yaml | 2 +-
web-console/console-config.js | 3 +-
web-console/package-lock.json | 14 +-
web-console/package.json | 2 +-
.../src/components/header-bar/header-bar.tsx | 3 +-
web-console/src/console-application.tsx | 63 ++++--
.../druid-models/query-context/query-context.tsx | 251 +++------------------
web-console/src/entry.tsx | 14 +-
.../src/helpers/execution/sql-task-execution.ts | 16 +-
web-console/src/utils/general.tsx | 6 +
web-console/src/utils/values-query.spec.tsx | 4 +-
web-console/src/utils/values-query.tsx | 21 +-
.../sql-data-loader-view/sql-data-loader-view.tsx | 12 +-
.../max-tasks-button/max-tasks-button.spec.tsx | 8 +-
.../max-tasks-button/max-tasks-button.tsx | 47 ++--
.../views/workbench-view/query-tab/query-tab.tsx | 32 ++-
.../views/workbench-view/run-panel/run-panel.tsx | 210 +++++++++--------
.../src/views/workbench-view/workbench-view.tsx | 46 ++--
18 files changed, 343 insertions(+), 411 deletions(-)
diff --git a/licenses.yaml b/licenses.yaml
index dcdac7bd187..0646c7131fd 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -5085,7 +5085,7 @@ license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
-version: 0.22.20
+version: 0.22.21
---
diff --git a/web-console/console-config.js b/web-console/console-config.js
index 10bdddb611a..25d99e7c650 100644
--- a/web-console/console-config.js
+++ b/web-console/console-config.js
@@ -17,6 +17,5 @@
*/
window.consoleConfig = {
- exampleManifestsUrl:
'https://druid.apache.org/data/example-manifests-v2.tsv',
- /* future configs may go here */
+ /* configs go here */
};
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 412f728d56d..e9319969b69 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -14,7 +14,7 @@
"@blueprintjs/datetime2": "^2.3.7",
"@blueprintjs/icons": "^5.10.0",
"@blueprintjs/select": "^5.2.1",
- "@druid-toolkit/query": "^0.22.20",
+ "@druid-toolkit/query": "^0.22.21",
"@druid-toolkit/visuals-core": "^0.3.3",
"@druid-toolkit/visuals-react": "^0.3.3",
"@fontsource/open-sans": "^5.0.28",
@@ -989,9 +989,9 @@
}
},
"node_modules/@druid-toolkit/query": {
- "version": "0.22.20",
- "resolved":
"https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.20.tgz",
- "integrity":
"sha512-GmmSd27y7zLVTjgTBQy+XoGeSSGhSDNmwyiwWtSua7I5LX8XqHV7Chi8HIH25YQoVgTK1pLK4RS8eRXxthRAzg==",
+ "version": "0.22.21",
+ "resolved":
"https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.21.tgz",
+ "integrity":
"sha512-4k0NGO2Ay90naSO8nyivPPvvhz73D/OkCo6So3frmPDLFfw5CYKSvAhy4RadtnLMZPwsnlVREjAmqbvBsHqgjQ==",
"dependencies": {
"tslib": "^2.5.2"
}
@@ -19093,9 +19093,9 @@
"dev": true
},
"@druid-toolkit/query": {
- "version": "0.22.20",
- "resolved":
"https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.20.tgz",
- "integrity":
"sha512-GmmSd27y7zLVTjgTBQy+XoGeSSGhSDNmwyiwWtSua7I5LX8XqHV7Chi8HIH25YQoVgTK1pLK4RS8eRXxthRAzg==",
+ "version": "0.22.21",
+ "resolved":
"https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.21.tgz",
+ "integrity":
"sha512-4k0NGO2Ay90naSO8nyivPPvvhz73D/OkCo6So3frmPDLFfw5CYKSvAhy4RadtnLMZPwsnlVREjAmqbvBsHqgjQ==",
"requires": {
"tslib": "^2.5.2"
}
diff --git a/web-console/package.json b/web-console/package.json
index 0c9370f8808..d55bb79d609 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -68,7 +68,7 @@
"@blueprintjs/datetime2": "^2.3.7",
"@blueprintjs/icons": "^5.10.0",
"@blueprintjs/select": "^5.2.1",
- "@druid-toolkit/query": "^0.22.20",
+ "@druid-toolkit/query": "^0.22.21",
"@druid-toolkit/visuals-core": "^0.3.3",
"@druid-toolkit/visuals-react": "^0.3.3",
"@fontsource/open-sans": "^5.0.28",
diff --git a/web-console/src/components/header-bar/header-bar.tsx
b/web-console/src/components/header-bar/header-bar.tsx
index e1b97cf4e13..aed66798299 100644
--- a/web-console/src/components/header-bar/header-bar.tsx
+++ b/web-console/src/components/header-bar/header-bar.tsx
@@ -59,7 +59,6 @@ import './header-bar.scss';
const capabilitiesOverride =
localStorageGetJson(LocalStorageKeys.CAPABILITIES_OVERRIDE);
export type HeaderActiveTab =
- | null
| 'data-loader'
| 'streaming-data-loader'
| 'classic-batch-data-loader'
@@ -93,7 +92,7 @@ const DruidLogo = React.memo(function DruidLogo() {
});
export interface HeaderBarProps {
- active: HeaderActiveTab;
+ active: HeaderActiveTab | null;
capabilities: Capabilities;
onUnrestrict(capabilities: Capabilities): void;
}
diff --git a/web-console/src/console-application.tsx
b/web-console/src/console-application.tsx
index 0d097729cbf..36a0b8aa392 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -28,7 +28,7 @@ import type { Filter } from 'react-table';
import type { HeaderActiveTab } from './components';
import { HeaderBar, Loader } from './components';
-import type { DruidEngine, QueryWithContext } from './druid-models';
+import type { DruidEngine, QueryContext, QueryWithContext } from
'./druid-models';
import { Capabilities, maybeGetClusterCapacity } from './helpers';
import { stringToTableFilters, tableFiltersToString } from './react-table';
import { AppToaster } from './singletons';
@@ -51,22 +51,32 @@ import './console-application.scss';
type FiltersRouteMatch = RouteComponentProps<{ filters?: string }>;
-function changeHashWithFilter(slug: string, filters: Filter[]) {
+function changeTabWithFilter(tab: HeaderActiveTab, filters: Filter[]) {
const filterString = tableFiltersToString(filters);
- location.hash = slug + (filterString ? `/${filterString}` : '');
+ location.hash = tab + (filterString ? `/${filterString}` : '');
}
-function viewFilterChange(slug: string) {
- return (filters: Filter[]) => changeHashWithFilter(slug, filters);
+function viewFilterChange(tab: HeaderActiveTab) {
+ return (filters: Filter[]) => changeTabWithFilter(tab, filters);
}
-function pathWithFilter(slug: string) {
- return [`/${slug}/:filters`, `/${slug}`];
+function pathWithFilter(tab: HeaderActiveTab) {
+ return [`/${tab}/:filters`, `/${tab}`];
+}
+
+function switchTab(tab: HeaderActiveTab) {
+ location.hash = tab;
+}
+
+function switchToWorkbenchTab(tabId: string) {
+ location.hash = `workbench/${tabId}`;
}
export interface ConsoleApplicationProps {
- defaultQueryContext?: Record<string, any>;
- mandatoryQueryContext?: Record<string, any>;
+ baseQueryContext?: QueryContext;
+ defaultQueryContext?: QueryContext;
+ mandatoryQueryContext?: QueryContext;
+ serverQueryContext?: QueryContext;
}
export interface ConsoleApplicationState {
@@ -158,22 +168,22 @@ export class ConsoleApplication extends
React.PureComponent<
private readonly goToStreamingDataLoader = (supervisorId?: string) => {
if (supervisorId) this.supervisorId = supervisorId;
- location.hash = 'streaming-data-loader';
+ switchTab('streaming-data-loader');
this.resetInitialsWithDelay();
};
private readonly goToClassicBatchDataLoader = (taskId?: string) => {
if (taskId) this.taskId = taskId;
- location.hash = 'classic-batch-data-loader';
+ switchTab('classic-batch-data-loader');
this.resetInitialsWithDelay();
};
private readonly goToDatasources = (datasource: string) => {
- changeHashWithFilter('datasources', [{ id: 'datasource', value:
`=${datasource}` }]);
+ changeTabWithFilter('datasources', [{ id: 'datasource', value:
`=${datasource}` }]);
};
private readonly goToSegments = (datasource: string, onlyUnavailable =
false) => {
- changeHashWithFilter(
+ changeTabWithFilter(
'segments',
compact([
{ id: 'datasource', value: `=${datasource}` },
@@ -183,19 +193,19 @@ export class ConsoleApplication extends
React.PureComponent<
};
private readonly goToSupervisor = (supervisorId: string) => {
- changeHashWithFilter('supervisors', [{ id: 'supervisor_id', value:
`=${supervisorId}` }]);
+ changeTabWithFilter('supervisors', [{ id: 'supervisor_id', value:
`=${supervisorId}` }]);
};
private readonly goToTasksWithTaskId = (taskId: string) => {
- changeHashWithFilter('tasks', [{ id: 'task_id', value: `=${taskId}` }]);
+ changeTabWithFilter('tasks', [{ id: 'task_id', value: `=${taskId}` }]);
};
private readonly goToTasksWithTaskGroupId = (taskGroupId: string) => {
- changeHashWithFilter('tasks', [{ id: 'group_id', value: `=${taskGroupId}`
}]);
+ changeTabWithFilter('tasks', [{ id: 'group_id', value: `=${taskGroupId}`
}]);
};
private readonly goToTasksWithDatasource = (datasource: string, type?:
string) => {
- changeHashWithFilter(
+ changeTabWithFilter(
'tasks',
compact([
{ id: 'datasource', value: `=${datasource}` },
@@ -206,24 +216,24 @@ export class ConsoleApplication extends
React.PureComponent<
private readonly openSupervisorSubmit = () => {
this.openSupervisorDialog = true;
- location.hash = 'supervisors';
+ switchTab('supervisors');
this.resetInitialsWithDelay();
};
private readonly openTaskSubmit = () => {
this.openTaskDialog = true;
- location.hash = 'tasks';
+ switchTab('tasks');
this.resetInitialsWithDelay();
};
private readonly goToQuery = (queryWithContext: QueryWithContext) => {
this.queryWithContext = queryWithContext;
- location.hash = 'workbench';
+ switchTab('workbench');
this.resetInitialsWithDelay();
};
private readonly wrapInViewContainer = (
- active: HeaderActiveTab,
+ active: HeaderActiveTab | null,
el: JSX.Element,
classType: 'normal' | 'narrow-pad' | 'thin' | 'thinner' = 'normal',
) => {
@@ -293,7 +303,8 @@ export class ConsoleApplication extends React.PureComponent<
};
private readonly wrappedWorkbenchView = (p: RouteComponentProps<{ tabId?:
string }>) => {
- const { defaultQueryContext, mandatoryQueryContext } = this.props;
+ const { defaultQueryContext, mandatoryQueryContext, baseQueryContext,
serverQueryContext } =
+ this.props;
const { capabilities } = this.state;
const queryEngines: DruidEngine[] = ['native'];
@@ -309,12 +320,12 @@ export class ConsoleApplication extends
React.PureComponent<
<WorkbenchView
capabilities={capabilities}
tabId={p.match.params.tabId}
- onTabChange={newTabId => {
- location.hash = `workbench/${newTabId}`;
- }}
+ onTabChange={switchToWorkbenchTab}
initQueryWithContext={this.queryWithContext}
defaultQueryContext={defaultQueryContext}
mandatoryQueryContext={mandatoryQueryContext}
+ baseQueryContext={baseQueryContext}
+ serverQueryContext={serverQueryContext}
queryEngines={queryEngines}
allowExplain
goToTask={this.goToTasksWithTaskId}
@@ -325,6 +336,7 @@ export class ConsoleApplication extends React.PureComponent<
};
private readonly wrappedSqlDataLoaderView = () => {
+ const { serverQueryContext } = this.props;
const { capabilities } = this.state;
return this.wrapInViewContainer(
'sql-data-loader',
@@ -334,6 +346,7 @@ export class ConsoleApplication extends React.PureComponent<
goToTask={this.goToTasksWithTaskId}
goToTaskGroup={this.goToTasksWithTaskGroupId}
getClusterCapacity={maybeGetClusterCapacity}
+ serverQueryContext={serverQueryContext}
/>,
);
};
diff --git a/web-console/src/druid-models/query-context/query-context.tsx
b/web-console/src/druid-models/query-context/query-context.tsx
index 17e8204e949..a25d268d845 100644
--- a/web-console/src/druid-models/query-context/query-context.tsx
+++ b/web-console/src/druid-models/query-context/query-context.tsx
@@ -16,9 +16,10 @@
* limitations under the License.
*/
-import { deepDelete, deepSet } from '../../utils';
-
+export type SelectDestination = 'taskReport' | 'durableStorage';
export type ArrayIngestMode = 'array' | 'mvd';
+export type TaskAssignment = 'auto' | 'max';
+export type SqlJoinAlgorithm = 'broadcast' | 'sortMerge';
export interface QueryContext {
useCache?: boolean;
@@ -30,15 +31,38 @@ export interface QueryContext {
// Multi-stage query
maxNumTasks?: number;
finalizeAggregations?: boolean;
- selectDestination?: string;
+ selectDestination?: SelectDestination;
durableShuffleStorage?: boolean;
maxParseExceptions?: number;
groupByEnableMultiValueUnnesting?: boolean;
arrayIngestMode?: ArrayIngestMode;
+ taskAssignment?: TaskAssignment;
+ sqlJoinAlgorithm?: SqlJoinAlgorithm;
+ failOnEmptyInsert?: boolean;
+ waitUntilSegmentsLoad?: boolean;
[key: string]: any;
}
+export const DEFAULT_SERVER_QUERY_CONTEXT: QueryContext = {
+ useCache: true,
+ populateCache: true,
+ useApproximateCountDistinct: true,
+ useApproximateTopN: true,
+ sqlTimeZone: 'Etc/UTC',
+
+ // Multi-stage query
+ finalizeAggregations: true,
+ selectDestination: 'taskReport',
+ durableShuffleStorage: false,
+ maxParseExceptions: 0,
+ groupByEnableMultiValueUnnesting: true,
+ taskAssignment: 'max',
+ sqlJoinAlgorithm: 'broadcast',
+ failOnEmptyInsert: false,
+ waitUntilSegmentsLoad: false,
+};
+
export interface QueryWithContext {
queryString: string;
queryContext?: QueryContext;
@@ -49,221 +73,10 @@ export function isEmptyContext(context: QueryContext |
undefined): boolean {
return !context || Object.keys(context).length === 0;
}
-// -----------------------------
-
-export function getUseCache(context: QueryContext): boolean {
- const { useCache } = context;
- return typeof useCache === 'boolean' ? useCache : true;
-}
-
-export function changeUseCache(context: QueryContext, useCache: boolean):
QueryContext {
- let newContext = context;
- if (useCache) {
- newContext = deepDelete(newContext, 'useCache');
- newContext = deepDelete(newContext, 'populateCache');
- } else {
- newContext = deepSet(newContext, 'useCache', false);
- newContext = deepSet(newContext, 'populateCache', false);
- }
- return newContext;
-}
-
-// -----------------------------
-
-export function getUseApproximateCountDistinct(context: QueryContext): boolean
{
- const { useApproximateCountDistinct } = context;
- return typeof useApproximateCountDistinct === 'boolean' ?
useApproximateCountDistinct : true;
-}
-
-export function changeUseApproximateCountDistinct(
- context: QueryContext,
- useApproximateCountDistinct: boolean,
-): QueryContext {
- if (useApproximateCountDistinct) {
- return deepDelete(context, 'useApproximateCountDistinct');
- } else {
- return deepSet(context, 'useApproximateCountDistinct', false);
- }
-}
-
-// -----------------------------
-
-export function getUseApproximateTopN(context: QueryContext): boolean {
- const { useApproximateTopN } = context;
- return typeof useApproximateTopN === 'boolean' ? useApproximateTopN : true;
-}
-
-export function changeUseApproximateTopN(
- context: QueryContext,
- useApproximateTopN: boolean,
-): QueryContext {
- if (useApproximateTopN) {
- return deepDelete(context, 'useApproximateTopN');
- } else {
- return deepSet(context, 'useApproximateTopN', false);
- }
-}
-
-// sqlTimeZone
-
-export function getTimezone(context: QueryContext): string | undefined {
- return context.sqlTimeZone;
-}
-
-export function changeTimezone(context: QueryContext, timezone: string |
undefined): QueryContext {
- if (timezone) {
- return deepSet(context, 'sqlTimeZone', timezone);
- } else {
- return deepDelete(context, 'sqlTimeZone');
- }
-}
-
-// maxNumTasks
-
-export function getMaxNumTasks(context: QueryContext): number | undefined {
- return context.maxNumTasks;
-}
-
-export function changeMaxNumTasks(
- context: QueryContext,
- maxNumTasks: number | undefined,
-): QueryContext {
- return typeof maxNumTasks === 'number'
- ? deepSet(context, 'maxNumTasks', maxNumTasks)
- : deepDelete(context, 'maxNumTasks');
-}
-
-// taskAssignment
-
-export function getTaskAssigment(context: QueryContext): string {
- const { taskAssignment } = context;
- return taskAssignment ?? 'max';
-}
-
-export function changeTaskAssigment(
- context: QueryContext,
- taskAssignment: string | undefined,
-): QueryContext {
- return typeof taskAssignment === 'string'
- ? deepSet(context, 'taskAssignment', taskAssignment)
- : deepDelete(context, 'taskAssignment');
-}
-
-// failOnEmptyInsert
-
-export function getFailOnEmptyInsert(context: QueryContext): boolean |
undefined {
- const { failOnEmptyInsert } = context;
- return typeof failOnEmptyInsert === 'boolean' ? failOnEmptyInsert :
undefined;
-}
-
-export function changeFailOnEmptyInsert(
- context: QueryContext,
- failOnEmptyInsert: boolean | undefined,
-): QueryContext {
- return typeof failOnEmptyInsert === 'boolean'
- ? deepSet(context, 'failOnEmptyInsert', failOnEmptyInsert)
- : deepDelete(context, 'failOnEmptyInsert');
-}
-
-// finalizeAggregations
-
-export function getFinalizeAggregations(context: QueryContext): boolean |
undefined {
- const { finalizeAggregations } = context;
- return typeof finalizeAggregations === 'boolean' ? finalizeAggregations :
undefined;
-}
-
-export function changeFinalizeAggregations(
- context: QueryContext,
- finalizeAggregations: boolean | undefined,
-): QueryContext {
- return typeof finalizeAggregations === 'boolean'
- ? deepSet(context, 'finalizeAggregations', finalizeAggregations)
- : deepDelete(context, 'finalizeAggregations');
-}
-
-// waitUntilSegmentsLoad
-
-export function getWaitUntilSegmentsLoad(context: QueryContext): boolean |
undefined {
- const { waitUntilSegmentsLoad } = context;
- return typeof waitUntilSegmentsLoad === 'boolean' ? waitUntilSegmentsLoad :
undefined;
-}
-
-export function changeWaitUntilSegmentsLoad(
- context: QueryContext,
- waitUntilSegmentsLoad: boolean | undefined,
-): QueryContext {
- return typeof waitUntilSegmentsLoad === 'boolean'
- ? deepSet(context, 'waitUntilSegmentsLoad', waitUntilSegmentsLoad)
- : deepDelete(context, 'waitUntilSegmentsLoad');
-}
-
-// groupByEnableMultiValueUnnesting
-
-export function getGroupByEnableMultiValueUnnesting(context: QueryContext):
boolean | undefined {
- const { groupByEnableMultiValueUnnesting } = context;
- return typeof groupByEnableMultiValueUnnesting === 'boolean'
- ? groupByEnableMultiValueUnnesting
- : undefined;
-}
-
-export function changeGroupByEnableMultiValueUnnesting(
- context: QueryContext,
- groupByEnableMultiValueUnnesting: boolean | undefined,
-): QueryContext {
- return typeof groupByEnableMultiValueUnnesting === 'boolean'
- ? deepSet(context, 'groupByEnableMultiValueUnnesting',
groupByEnableMultiValueUnnesting)
- : deepDelete(context, 'groupByEnableMultiValueUnnesting');
-}
-
-// durableShuffleStorage
-
-export function getDurableShuffleStorage(context: QueryContext): boolean {
- const { durableShuffleStorage } = context;
- return Boolean(durableShuffleStorage);
-}
-
-export function changeDurableShuffleStorage(
- context: QueryContext,
- durableShuffleStorage: boolean,
-): QueryContext {
- if (durableShuffleStorage) {
- return deepSet(context, 'durableShuffleStorage', true);
- } else {
- return deepDelete(context, 'durableShuffleStorage');
- }
-}
-
-// maxParseExceptions
-
-export function getMaxParseExceptions(context: QueryContext): number {
- const { maxParseExceptions } = context;
- return Number(maxParseExceptions) || 0;
-}
-
-export function changeMaxParseExceptions(
- context: QueryContext,
- maxParseExceptions: number,
-): QueryContext {
- if (maxParseExceptions !== 0) {
- return deepSet(context, 'maxParseExceptions', maxParseExceptions);
- } else {
- return deepDelete(context, 'maxParseExceptions');
- }
-}
-
-// arrayIngestMode
-
-export function getArrayIngestMode(context: QueryContext): ArrayIngestMode |
undefined {
- return context.arrayIngestMode;
-}
-
-export function changeArrayIngestMode(
+export function getQueryContextKey(
+ key: keyof QueryContext,
context: QueryContext,
- arrayIngestMode: ArrayIngestMode | undefined,
-): QueryContext {
- if (arrayIngestMode) {
- return deepSet(context, 'arrayIngestMode', arrayIngestMode);
- } else {
- return deepDelete(context, 'arrayIngestMode');
- }
+ defaultContext: QueryContext,
+): any {
+ return typeof context[key] !== 'undefined' ? context[key] :
defaultContext[key];
}
diff --git a/web-console/src/entry.tsx b/web-console/src/entry.tsx
index 25518ecdeb9..0e698a3f84b 100644
--- a/web-console/src/entry.tsx
+++ b/web-console/src/entry.tsx
@@ -28,6 +28,7 @@ import { createRoot } from 'react-dom/client';
import { bootstrapJsonParse } from './bootstrap/json-parser';
import { bootstrapReactTable } from './bootstrap/react-table-defaults';
import { ConsoleApplication } from './console-application';
+import type { QueryContext } from './druid-models';
import type { Links } from './links';
import { setLinkOverrides } from './links';
import { Api, UrlBaser } from './singletons';
@@ -55,11 +56,16 @@ interface ConsoleConfig {
// A set of custom headers name/value to set on every AJAX request
customHeaders?: Record<string, string>;
- // The query context to set if the user does not have one saved in local
storage, defaults to {}
- defaultQueryContext?: Record<string, any>;
+ baseQueryContext?: QueryContext;
+
+ // The query context to set one new query tabs
+ defaultQueryContext?: QueryContext;
// Extra context properties that will be added to all query requests
- mandatoryQueryContext?: Record<string, any>;
+ mandatoryQueryContext?: QueryContext;
+
+ // The default context that is set by the server
+ serverQueryContext?: QueryContext;
// Allow for link overriding to different docs
linkOverrides?: Links;
@@ -104,8 +110,10 @@ QueryRunner.defaultQueryExecutor = (payload, isSql,
cancelToken) => {
createRoot(container).render(
<OverlaysProvider>
<ConsoleApplication
+ baseQueryContext={consoleConfig.baseQueryContext}
defaultQueryContext={consoleConfig.defaultQueryContext}
mandatoryQueryContext={consoleConfig.mandatoryQueryContext}
+ serverQueryContext={consoleConfig.serverQueryContext}
/>
</OverlaysProvider>,
);
diff --git a/web-console/src/helpers/execution/sql-task-execution.ts
b/web-console/src/helpers/execution/sql-task-execution.ts
index 0fa9b090959..f4dd45a2cb9 100644
--- a/web-console/src/helpers/execution/sql-task-execution.ts
+++ b/web-console/src/helpers/execution/sql-task-execution.ts
@@ -36,6 +36,7 @@ function ensureExecutionModeIsSet(context: QueryContext |
undefined): QueryConte
export interface SubmitTaskQueryOptions {
query: string | Record<string, any>;
context?: QueryContext;
+ baseQueryContext?: QueryContext;
prefixLines?: number;
cancelToken?: CancelToken;
preserveOnTermination?: boolean;
@@ -45,7 +46,15 @@ export interface SubmitTaskQueryOptions {
export async function submitTaskQuery(
options: SubmitTaskQueryOptions,
): Promise<Execution | IntermediateQueryState<Execution>> {
- const { query, context, prefixLines, cancelToken, preserveOnTermination,
onSubmitted } = options;
+ const {
+ query,
+ context,
+ baseQueryContext,
+ prefixLines,
+ cancelToken,
+ preserveOnTermination,
+ onSubmitted,
+ } = options;
let sqlQuery: string;
let jsonQuery: Record<string, any>;
@@ -53,7 +62,7 @@ export async function submitTaskQuery(
sqlQuery = query;
jsonQuery = {
query: sqlQuery,
- context: ensureExecutionModeIsSet(context),
+ context: ensureExecutionModeIsSet({ ...baseQueryContext, ...context }),
resultFormat: 'array',
header: true,
typesHeader: true,
@@ -65,6 +74,7 @@ export async function submitTaskQuery(
jsonQuery = {
...query,
context: ensureExecutionModeIsSet({
+ ...baseQueryContext,
...query.context,
...context,
}),
@@ -96,7 +106,7 @@ export async function submitTaskQuery(
);
}
- const execution = Execution.fromAsyncStatus(sqlAsyncStatus, sqlQuery,
context);
+ const execution = Execution.fromAsyncStatus(sqlAsyncStatus, sqlQuery,
jsonQuery.context);
if (onSubmitted) {
onSubmitted(execution.id);
diff --git a/web-console/src/utils/general.tsx
b/web-console/src/utils/general.tsx
index a3256c3ab11..7698d3c3af8 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -389,6 +389,12 @@ export function assemble<T>(...xs: (T | undefined | false
| null | '')[]): T[] {
return compact(xs);
}
+export function removeUndefinedValues<T extends Record<string, any>>(obj: T):
Partial<T> {
+ return Object.fromEntries(
+ Object.entries(obj).filter(([_, value]) => value !== undefined),
+ ) as Partial<T>;
+}
+
export function moveToEnd<T>(
xs: T[],
predicate: (value: T, index: number, array: T[]) => unknown,
diff --git a/web-console/src/utils/values-query.spec.tsx
b/web-console/src/utils/values-query.spec.tsx
index 99884f382c1..7bc093bc3e8 100644
--- a/web-console/src/utils/values-query.spec.tsx
+++ b/web-console/src/utils/values-query.spec.tsx
@@ -45,6 +45,7 @@ describe('queryResultToValuesQuery', () => {
[2, 3],
null,
],
+ [null, null, null, null, null, null, null],
],
false,
true,
@@ -64,7 +65,8 @@ describe('queryResultToValuesQuery', () => {
FROM (
VALUES
('2022-02-01T00:00:00.000Z', 'brokerA.internal', 'broker',
'{"type":"sys","swap/free":1223334,"swap/max":3223334}', 'es<#>es-419', '1',
NULL),
- ('2022-02-01T00:00:00.000Z', 'brokerA.internal', 'broker',
'{"type":"query","time":1223,"bytes":2434234}', 'en<#>es<#>es-419', '2<#>3',
NULL)
+ ('2022-02-01T00:00:00.000Z', 'brokerA.internal', 'broker',
'{"type":"query","time":1223,"bytes":2434234}', 'en<#>es<#>es-419', '2<#>3',
NULL),
+ (NULL, NULL, NULL, NULL, NULL, NULL, NULL)
) AS "t" ("c1", "c2", "c3", "c4", "c5", "c6", "c7")
`);
});
diff --git a/web-console/src/utils/values-query.tsx
b/web-console/src/utils/values-query.tsx
index 2f1a5f699ca..1b5e62b44c2 100644
--- a/web-console/src/utils/values-query.tsx
+++ b/web-console/src/utils/values-query.tsx
@@ -65,28 +65,29 @@ export function queryResultToValuesQuery(sample:
QueryResult): SqlQuery {
expression: SqlValues.create(
rows.map(row =>
SqlRecord.create(
- row.map((r, i) => {
+ row.map((d, i) => {
+ if (d == null) return L.NULL;
const column = header[i];
const { nativeType } = column;
const sqlType = getEffectiveSqlType(column);
if (nativeType === 'COMPLEX<json>') {
- return L(isJsonString(r) ? r : JSONBig.stringify(r));
+ return L(isJsonString(d) ? d : JSONBig.stringify(d));
} else if (String(sqlType).endsWith(' ARRAY')) {
- return L(r.join(SAMPLE_ARRAY_SEPARATOR));
+ return L(d.join(SAMPLE_ARRAY_SEPARATOR));
} else if (
sqlType === 'OTHER' &&
String(nativeType).startsWith('COMPLEX<') &&
- typeof r === 'string' &&
- r.startsWith('"') &&
- r.endsWith('"')
+ typeof d === 'string' &&
+ d.startsWith('"') &&
+ d.endsWith('"')
) {
- // r is a JSON encoded base64 string
- return L(r.slice(1, -1));
- } else if (typeof r === 'object') {
+ // d is a JSON encoded base64 string
+ return L(d.slice(1, -1));
+ } else if (typeof d === 'object') {
// Cleanup array if it happens to get here, it shouldn't.
return L.NULL;
} else {
- return L(r);
+ return L(d);
}
}),
),
diff --git
a/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
b/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
index 6cc00957b8d..2e10734a170 100644
--- a/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
+++ b/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
@@ -30,6 +30,7 @@ import type {
QueryWithContext,
} from '../../druid-models';
import {
+ DEFAULT_SERVER_QUERY_CONTEXT,
Execution,
externalConfigToIngestQueryPattern,
ingestQueryPatternToQuery,
@@ -65,12 +66,20 @@ export interface SqlDataLoaderViewProps {
goToTask(taskId: string): void;
goToTaskGroup(taskGroupId: string): void;
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
+ serverQueryContext?: QueryContext;
}
export const SqlDataLoaderView = React.memo(function SqlDataLoaderView(
props: SqlDataLoaderViewProps,
) {
- const { capabilities, goToQuery, goToTask, goToTaskGroup, getClusterCapacity
} = props;
+ const {
+ capabilities,
+ goToQuery,
+ goToTask,
+ goToTaskGroup,
+ getClusterCapacity,
+ serverQueryContext = DEFAULT_SERVER_QUERY_CONTEXT,
+ } = props;
const [alertElement, setAlertElement] = useState<JSX.Element | undefined>();
const [externalConfigStep, setExternalConfigStep] =
useState<Partial<ExternalConfig>>({});
const [content, setContent] = useLocalStorageState<LoaderContent |
undefined>(
@@ -187,6 +196,7 @@ export const SqlDataLoaderView = React.memo(function
SqlDataLoaderView(
clusterCapacity={capabilities.getMaxTaskSlots()}
queryContext={content.queryContext || {}}
changeQueryContext={queryContext => setContent({ ...content,
queryContext })}
+ defaultQueryContext={serverQueryContext}
minimal
/>
}
diff --git
a/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.spec.tsx
b/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.spec.tsx
index 5954c1f3f9b..1ae864dee06 100644
---
a/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.spec.tsx
+++
b/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.spec.tsx
@@ -18,6 +18,7 @@
import React from 'react';
+import { DEFAULT_SERVER_QUERY_CONTEXT } from '../../../druid-models';
import { shallow } from '../../../utils/shallow-renderer';
import { MaxTasksButton } from './max-tasks-button';
@@ -25,7 +26,12 @@ import { MaxTasksButton } from './max-tasks-button';
describe('MaxTasksButton', () => {
it('matches snapshot', () => {
const comp = shallow(
- <MaxTasksButton clusterCapacity={6} queryContext={{}}
changeQueryContext={() => {}} />,
+ <MaxTasksButton
+ clusterCapacity={6}
+ queryContext={{}}
+ changeQueryContext={() => {}}
+ defaultQueryContext={DEFAULT_SERVER_QUERY_CONTEXT}
+ />,
);
expect(comp).toMatchSnapshot();
diff --git
a/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.tsx
b/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.tsx
index ea239bc3a26..c84f9f00e37 100644
--- a/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.tsx
+++ b/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.tsx
@@ -19,37 +19,38 @@
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 React, { useState } from 'react';
import { NumericInputDialog } from '../../../dialogs';
-import type { QueryContext } from '../../../druid-models';
-import {
- changeMaxNumTasks,
- changeTaskAssigment,
- getMaxNumTasks,
- getTaskAssigment,
-} from '../../../druid-models';
-import { formatInteger, tickIcon } from '../../../utils';
+import type { QueryContext, TaskAssignment } from '../../../druid-models';
+import { getQueryContextKey } from '../../../druid-models';
+import { 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 = ['max', 'auto'];
+const TASK_ASSIGNMENT_OPTIONS: TaskAssignment[] = ['max', 'auto'];
const TASK_ASSIGNMENT_DESCRIPTION: Record<string, string> = {
max: 'Use as many tasks as possible, up to the maximum.',
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) => {
+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;
- maxNumLabelFn?: (maxNum: number) => { text: string; label?: string };
+ maxTasksLabelFn?: (maxNum: number) => { text: string; label?: string };
+ fullClusterCapacityLabelFn?: (clusterCapacity: number) => string;
}
export const MaxTasksButton = function MaxTasksButton(props:
MaxTasksButtonProps) {
@@ -57,19 +58,19 @@ export const MaxTasksButton = function
MaxTasksButton(props: MaxTasksButtonProps
clusterCapacity,
queryContext,
changeQueryContext,
+ defaultQueryContext,
menuHeader,
- maxNumLabelFn = DEFAULT_MAX_NUM_LABEL_FN,
+ maxTasksLabelFn = DEFAULT_MAX_NUM_TASKS_LABEL_FN,
+ fullClusterCapacityLabelFn = DEFAULT_FULL_CLUSTER_CAPACITY_LABEL_FN,
...rest
} = props;
const [customMaxNumTasksDialogOpen, setCustomMaxNumTasksDialogOpen] =
useState(false);
- const maxNumTasks = getMaxNumTasks(queryContext);
- const taskAssigment = getTaskAssigment(queryContext);
+ const maxNumTasks = queryContext.maxNumTasks;
+ const taskAssigment = getQueryContextKey('taskAssignment', queryContext,
defaultQueryContext);
const fullClusterCapacity =
- typeof clusterCapacity === 'number'
- ? `${formatInteger(clusterCapacity)} (full cluster capacity)`
- : undefined;
+ typeof clusterCapacity === 'number' ?
fullClusterCapacityLabelFn(clusterCapacity) : undefined;
const shownMaxNumTaskOptions = clusterCapacity
? MAX_NUM_TASK_OPTIONS.filter(_ => _ <= clusterCapacity)
@@ -88,11 +89,11 @@ export const MaxTasksButton = function
MaxTasksButton(props: MaxTasksButtonProps
<MenuItem
icon={tickIcon(typeof maxNumTasks === 'undefined')}
text={fullClusterCapacity}
- onClick={() =>
changeQueryContext(changeMaxNumTasks(queryContext, undefined))}
+ onClick={() => changeQueryContext(deleteKeys(queryContext,
['maxNumTasks']))}
/>
)}
{shownMaxNumTaskOptions.map(m => {
- const { text, label } = maxNumLabelFn(m);
+ const { text, label } = maxTasksLabelFn(m);
return (
<MenuItem
@@ -100,7 +101,7 @@ export const MaxTasksButton = function
MaxTasksButton(props: MaxTasksButtonProps
icon={tickIcon(m === maxNumTasks)}
text={text}
label={label}
- onClick={() =>
changeQueryContext(changeMaxNumTasks(queryContext, m))}
+ onClick={() => changeQueryContext({ ...queryContext,
maxNumTasks: m })}
/>
);
})}
@@ -124,7 +125,7 @@ export const MaxTasksButton = function
MaxTasksButton(props: MaxTasksButtonProps
}
shouldDismissPopover={false}
multiline
- onClick={() =>
changeQueryContext(changeTaskAssigment(queryContext, t))}
+ onClick={() => changeQueryContext({ ...queryContext,
taskAssignment: t })}
/>
))}
</MenuItem>
@@ -158,8 +159,8 @@ export const MaxTasksButton = function
MaxTasksButton(props: MaxTasksButtonProps
minValue={2}
integer
initValue={maxNumTasks || 2}
- onSubmit={p => {
- changeQueryContext(changeMaxNumTasks(queryContext, p));
+ onSubmit={maxNumTasks => {
+ changeQueryContext({ ...queryContext, maxNumTasks });
}}
onClose={() => setCustomMaxNumTasksDialogOpen(false)}
/>
diff --git a/web-console/src/views/workbench-view/query-tab/query-tab.tsx
b/web-console/src/views/workbench-view/query-tab/query-tab.tsx
index cf863a14387..acdfa67fad2 100644
--- a/web-console/src/views/workbench-view/query-tab/query-tab.tsx
+++ b/web-console/src/views/workbench-view/query-tab/query-tab.tsx
@@ -21,14 +21,14 @@ import { IconNames } from '@blueprintjs/icons';
import type { QueryResult } from '@druid-toolkit/query';
import { QueryRunner, SqlQuery } from '@druid-toolkit/query';
import axios from 'axios';
-import type { ComponentProps, JSX } from 'react';
+import type { JSX } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import SplitterLayout from 'react-splitter-layout';
import { useStore } from 'zustand';
import { Loader, QueryErrorPane } from '../../../components';
import type { CapacityInfo, DruidEngine, LastExecution, QueryContext } from
'../../../druid-models';
-import { Execution, WorkbenchQuery } from '../../../druid-models';
+import { DEFAULT_SERVER_QUERY_CONTEXT, Execution, WorkbenchQuery } from
'../../../druid-models';
import {
executionBackgroundStatusCheck,
reattachTaskExecution,
@@ -60,6 +60,7 @@ import { FlexibleQueryInput } from
'../flexible-query-input/flexible-query-input
import { IngestSuccessPane } from '../ingest-success-pane/ingest-success-pane';
import { metadataStateStore } from '../metadata-state-store';
import { ResultTablePane } from '../result-table-pane/result-table-pane';
+import type { RunPanelProps } from '../run-panel/run-panel';
import { RunPanel } from '../run-panel/run-panel';
import { workStateStore } from '../work-state-store';
@@ -69,10 +70,16 @@ const queryRunner = new QueryRunner({
inflateDateStrategy: 'none',
});
-export interface QueryTabProps {
+export interface QueryTabProps
+ extends Pick<
+ RunPanelProps,
+ 'maxTasksMenuHeader' | 'enginesLabelFn' | 'maxTasksLabelFn' |
'fullClusterCapacityLabelFn'
+ > {
query: WorkbenchQuery;
id: string;
mandatoryQueryContext: QueryContext | undefined;
+ baseQueryContext: QueryContext | undefined;
+ serverQueryContext: QueryContext;
columnMetadata: readonly ColumnMetadata[] | undefined;
onQueryChange(newQuery: WorkbenchQuery): void;
onQueryTab(newQuery: WorkbenchQuery, tabName?: string): void;
@@ -82,9 +89,6 @@ export interface QueryTabProps {
clusterCapacity: number | undefined;
goToTask(taskId: string): void;
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) {
@@ -93,6 +97,8 @@ export const QueryTab = React.memo(function QueryTab(props:
QueryTabProps) {
id,
columnMetadata,
mandatoryQueryContext,
+ baseQueryContext,
+ serverQueryContext = DEFAULT_SERVER_QUERY_CONTEXT,
onQueryChange,
onQueryTab,
onDetails,
@@ -101,9 +107,10 @@ export const QueryTab = React.memo(function
QueryTab(props: QueryTabProps) {
clusterCapacity,
goToTask,
getClusterCapacity,
- maxTaskMenuHeader,
+ maxTasksMenuHeader,
enginesLabelFn,
- maxTaskLabelFn,
+ maxTasksLabelFn,
+ fullClusterCapacityLabelFn,
} = props;
const [alertElement, setAlertElement] = useState<JSX.Element | undefined>();
@@ -196,6 +203,8 @@ export const QueryTab = React.memo(function QueryTab(props:
QueryTabProps) {
case 'sql-msq-task':
return await submitTaskQuery({
query,
+ context: mandatoryQueryContext,
+ baseQueryContext,
prefixLines,
cancelToken,
preserveOnTermination: true,
@@ -227,6 +236,7 @@ export const QueryTab = React.memo(function QueryTab(props:
QueryTabProps) {
const resultPromise = queryRunner.runQuery({
query,
extraQueryContext: mandatoryQueryContext,
+ defaultQueryContext: baseQueryContext,
cancelToken: new axios.CancelToken(cancelFn => {
nativeQueryCancelFnRef.current = cancelFn;
}),
@@ -404,10 +414,12 @@ export const QueryTab = React.memo(function
QueryTab(props: QueryTabProps) {
running={executionState.loading}
queryEngines={queryEngines}
clusterCapacity={clusterCapacity}
+ defaultQueryContext={{ ...serverQueryContext,
...baseQueryContext }}
moreMenu={runMoreMenu}
- maxTaskMenuHeader={maxTaskMenuHeader}
+ maxTasksMenuHeader={maxTasksMenuHeader}
enginesLabelFn={enginesLabelFn}
- maxTaskLabelFn={maxTaskLabelFn}
+ maxTasksLabelFn={maxTasksLabelFn}
+ fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
/>
{executionState.isLoading() && (
<ExecutionTimerPanel
diff --git a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
index f8235379612..9ed135da354 100644
--- a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
+++ b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
@@ -30,7 +30,7 @@ import {
useHotkeys,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
-import type { ComponentProps, JSX } from 'react';
+import type { JSX } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { MenuCheckbox, MenuTristate } from '../../../components';
@@ -41,35 +41,14 @@ import type {
DruidEngine,
IndexSpec,
QueryContext,
+ SelectDestination,
+ SqlJoinAlgorithm,
WorkbenchQuery,
} from '../../../druid-models';
-import {
- changeArrayIngestMode,
- changeDurableShuffleStorage,
- changeFailOnEmptyInsert,
- changeFinalizeAggregations,
- changeGroupByEnableMultiValueUnnesting,
- changeMaxParseExceptions,
- changeTimezone,
- changeUseApproximateCountDistinct,
- changeUseApproximateTopN,
- changeUseCache,
- changeWaitUntilSegmentsLoad,
- getArrayIngestMode,
- getDurableShuffleStorage,
- getFailOnEmptyInsert,
- getFinalizeAggregations,
- getGroupByEnableMultiValueUnnesting,
- getMaxParseExceptions,
- getTimezone,
- getUseApproximateCountDistinct,
- getUseApproximateTopN,
- getUseCache,
- getWaitUntilSegmentsLoad,
- summarizeIndexSpec,
-} from '../../../druid-models';
+import { getQueryContextKey, summarizeIndexSpec } from '../../../druid-models';
import { getLink } from '../../../links';
-import { deepGet, deepSet, pluralIfNeeded, tickIcon } from '../../../utils';
+import { deepGet, pluralIfNeeded, removeUndefinedValues, tickIcon } from
'../../../utils';
+import type { MaxTasksButtonProps } from
'../max-tasks-button/max-tasks-button';
import { MaxTasksButton } from '../max-tasks-button/max-tasks-button';
import { QueryParametersDialog } from
'../query-parameters-dialog/query-parameters-dialog';
@@ -119,7 +98,8 @@ const DEFAULT_ENGINES_LABEL_FN = (engine: DruidEngine |
undefined) => {
};
};
-export interface RunPanelProps {
+export interface RunPanelProps
+ extends Pick<MaxTasksButtonProps, 'maxTasksLabelFn' |
'fullClusterCapacityLabelFn'> {
query: WorkbenchQuery;
onQueryChange(query: WorkbenchQuery): void;
running: boolean;
@@ -127,10 +107,10 @@ export interface RunPanelProps {
onRun(preview: boolean): void | Promise<void>;
queryEngines: DruidEngine[];
clusterCapacity: number | undefined;
+ defaultQueryContext: QueryContext;
moreMenu?: JSX.Element;
- maxTaskMenuHeader?: JSX.Element;
+ maxTasksMenuHeader?: JSX.Element;
enginesLabelFn?: (engine: DruidEngine | undefined) => { text: string;
label?: string };
- maxTaskLabelFn?: ComponentProps<typeof MaxTasksButton>['maxNumLabelFn'];
}
export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
@@ -143,9 +123,11 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
small,
queryEngines,
clusterCapacity,
- maxTaskMenuHeader,
- maxTaskLabelFn,
+ defaultQueryContext,
+ maxTasksMenuHeader,
enginesLabelFn = DEFAULT_ENGINES_LABEL_FN,
+ maxTasksLabelFn,
+ fullClusterCapacityLabelFn,
} = props;
const [editContextDialogOpen, setEditContextDialogOpen] = useState(false);
const [editParametersDialogOpen, setEditParametersDialogOpen] =
useState(false);
@@ -158,20 +140,52 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
const numContextKeys = Object.keys(queryContext).length;
const queryParameters = query.queryParameters;
- const arrayIngestMode = getArrayIngestMode(queryContext);
- const maxParseExceptions = getMaxParseExceptions(queryContext);
- const failOnEmptyInsert = getFailOnEmptyInsert(queryContext);
- const finalizeAggregations = getFinalizeAggregations(queryContext);
- const waitUntilSegmentsLoad = getWaitUntilSegmentsLoad(queryContext);
- const groupByEnableMultiValueUnnesting =
getGroupByEnableMultiValueUnnesting(queryContext);
- const sqlJoinAlgorithm = queryContext.sqlJoinAlgorithm ?? 'broadcast';
- const selectDestination = queryContext.selectDestination ?? 'taskReport';
- const durableShuffleStorage = getDurableShuffleStorage(queryContext);
+ // Extract the context parts that have UI
+ const sqlTimeZone = queryContext.sqlTimeZone;
+
+ const useCache = getQueryContextKey('useCache', queryContext,
defaultQueryContext);
+ const useApproximateTopN = getQueryContextKey(
+ 'useApproximateTopN',
+ queryContext,
+ defaultQueryContext,
+ );
+ const useApproximateCountDistinct = getQueryContextKey(
+ 'useApproximateCountDistinct',
+ queryContext,
+ defaultQueryContext,
+ );
+
+ const arrayIngestMode = queryContext.arrayIngestMode;
+ const maxParseExceptions = getQueryContextKey(
+ 'maxParseExceptions',
+ queryContext,
+ defaultQueryContext,
+ );
+ const failOnEmptyInsert = getQueryContextKey(
+ 'failOnEmptyInsert',
+ queryContext,
+ defaultQueryContext,
+ );
+ const finalizeAggregations = queryContext.finalizeAggregations;
+ const waitUntilSegmentsLoad = queryContext.waitUntilSegmentsLoad;
+ const groupByEnableMultiValueUnnesting =
queryContext.groupByEnableMultiValueUnnesting;
+ const sqlJoinAlgorithm = getQueryContextKey(
+ 'sqlJoinAlgorithm',
+ queryContext,
+ defaultQueryContext,
+ );
+ const selectDestination = getQueryContextKey(
+ 'selectDestination',
+ queryContext,
+ defaultQueryContext,
+ );
+ const durableShuffleStorage = getQueryContextKey(
+ 'durableShuffleStorage',
+ queryContext,
+ defaultQueryContext,
+ );
+
const indexSpec: IndexSpec | undefined = deepGet(queryContext, 'indexSpec');
- const useApproximateCountDistinct =
getUseApproximateCountDistinct(queryContext);
- const useApproximateTopN = getUseApproximateTopN(queryContext);
- const useCache = getUseCache(queryContext);
- const timezone = getTimezone(queryContext);
const handleRun = useCallback(() => {
if (!onRun) return;
@@ -210,7 +224,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
const queryEngine = query.engine;
function changeQueryContext(queryContext: QueryContext) {
- onQueryChange(query.changeQueryContext(queryContext));
+
onQueryChange(query.changeQueryContext(removeUndefinedValues(queryContext)));
}
function offsetOptions(): JSX.Element[] {
@@ -221,10 +235,10 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
items.push(
<MenuItem
key={offset}
- icon={tickIcon(offset === timezone)}
+ icon={tickIcon(offset === sqlTimeZone)}
text={offset}
shouldDismissPopover={false}
- onClick={() => changeQueryContext(changeTimezone(queryContext,
offset))}
+ onClick={() => changeQueryContext({ ...queryContext, sqlTimeZone:
offset })}
/>,
);
}
@@ -315,29 +329,32 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
<MenuItem
icon={IconNames.GLOBE_NETWORK}
text="Timezone"
- label={timezone || 'default'}
+ label={sqlTimeZone ?? defaultQueryContext.sqlTimeZone}
>
<MenuDivider title="Timezone type" />
<MenuItem
- icon={tickIcon(!timezone)}
+ icon={tickIcon(!sqlTimeZone)}
text="Default"
+ label={defaultQueryContext.sqlTimeZone}
shouldDismissPopover={false}
- onClick={() =>
changeQueryContext(changeTimezone(queryContext, undefined))}
+ onClick={() =>
+ changeQueryContext({ ...queryContext, sqlTimeZone:
undefined })
+ }
/>
- <MenuItem icon={tickIcon(String(timezone).includes('/'))}
text="Named">
+ <MenuItem
icon={tickIcon(String(sqlTimeZone).includes('/'))} text="Named">
{NAMED_TIMEZONES.map(namedTimezone => (
<MenuItem
key={namedTimezone}
- icon={tickIcon(namedTimezone === timezone)}
+ icon={tickIcon(namedTimezone === sqlTimeZone)}
text={namedTimezone}
shouldDismissPopover={false}
onClick={() =>
- changeQueryContext(changeTimezone(queryContext,
namedTimezone))
+ changeQueryContext({ ...queryContext, sqlTimeZone:
namedTimezone })
}
/>
))}
</MenuItem>
- <MenuItem icon={tickIcon(String(timezone).includes(':'))}
text="Offset">
+ <MenuItem
icon={tickIcon(String(sqlTimeZone).includes(':'))} text="Offset">
{offsetOptions()}
</MenuItem>
<MenuItem
@@ -360,7 +377,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
icon={tickIcon(v === maxParseExceptions)}
text={v === -1 ? '∞ (-1)' : String(v)}
onClick={() =>
-
changeQueryContext(changeMaxParseExceptions(queryContext, v))
+ changeQueryContext({ ...queryContext,
maxParseExceptions: v })
}
shouldDismissPopover={false}
/>
@@ -371,8 +388,8 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
text="Fail on empty insert"
value={failOnEmptyInsert}
undefinedEffectiveValue={false}
- onValueChange={v =>
-
changeQueryContext(changeFailOnEmptyInsert(queryContext, v))
+ onValueChange={failOnEmptyInsert =>
+ changeQueryContext({ ...queryContext,
failOnEmptyInsert })
}
/>
<MenuTristate
@@ -380,8 +397,8 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
text="Finalize aggregations"
value={finalizeAggregations}
undefinedEffectiveValue={!ingestMode}
- onValueChange={v =>
-
changeQueryContext(changeFinalizeAggregations(queryContext, v))
+ onValueChange={finalizeAggregations =>
+ changeQueryContext({ ...queryContext,
finalizeAggregations })
}
/>
<MenuTristate
@@ -389,8 +406,8 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
text="Wait until segments have loaded"
value={waitUntilSegmentsLoad}
undefinedEffectiveValue={ingestMode}
- onValueChange={v =>
-
changeQueryContext(changeWaitUntilSegmentsLoad(queryContext, v))
+ onValueChange={waitUntilSegmentsLoad =>
+ changeQueryContext({ ...queryContext,
waitUntilSegmentsLoad })
}
/>
<MenuTristate
@@ -398,8 +415,8 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
text="Enable GroupBy multi-value unnesting"
value={groupByEnableMultiValueUnnesting}
undefinedEffectiveValue={!ingestMode}
- onValueChange={v =>
-
changeQueryContext(changeGroupByEnableMultiValueUnnesting(queryContext, v))
+ onValueChange={groupByEnableMultiValueUnnesting =>
+ changeQueryContext({ ...queryContext,
groupByEnableMultiValueUnnesting })
}
/>
<MenuItem
@@ -407,14 +424,14 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
text="Join algorithm"
label={sqlJoinAlgorithm}
>
- {['broadcast', 'sortMerge'].map(o => (
+ {(['broadcast', 'sortMerge'] as
SqlJoinAlgorithm[]).map(o => (
<MenuItem
key={o}
icon={tickIcon(sqlJoinAlgorithm === o)}
text={o}
shouldDismissPopover={false}
onClick={() =>
- changeQueryContext(deepSet(queryContext,
'sqlJoinAlgorithm', o))
+ changeQueryContext({ ...queryContext,
sqlJoinAlgorithm: o })
}
/>
))}
@@ -425,14 +442,14 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
label={selectDestination}
intent={intent}
>
- {['taskReport', 'durableStorage'].map(o => (
+ {(['taskReport', 'durableStorage'] as
SelectDestination[]).map(o => (
<MenuItem
key={o}
icon={tickIcon(selectDestination === o)}
text={o}
shouldDismissPopover={false}
onClick={() =>
- changeQueryContext(deepSet(queryContext,
'selectDestination', o))
+ changeQueryContext({ ...queryContext,
selectDestination: o })
}
/>
))}
@@ -454,9 +471,10 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
checked={durableShuffleStorage}
text="Durable shuffle storage"
onChange={() =>
- changeQueryContext(
- changeDurableShuffleStorage(queryContext,
!durableShuffleStorage),
- )
+ changeQueryContext({
+ ...queryContext,
+ durableShuffleStorage: !durableShuffleStorage,
+ })
}
/>
<MenuItem
@@ -474,15 +492,22 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
<MenuCheckbox
checked={useCache}
text="Use cache"
- onChange={() =>
changeQueryContext(changeUseCache(queryContext, !useCache))}
+ onChange={() =>
+ changeQueryContext({
+ ...queryContext,
+ useCache: !useCache,
+ populateCache: !useCache,
+ })
+ }
/>
<MenuCheckbox
checked={useApproximateTopN}
text="Use approximate TopN"
onChange={() =>
- changeQueryContext(
- changeUseApproximateTopN(queryContext,
!useApproximateTopN),
- )
+ changeQueryContext({
+ ...queryContext,
+ useApproximateTopN: !useApproximateTopN,
+ })
}
/>
</>
@@ -492,12 +517,10 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
checked={useApproximateCountDistinct}
text="Use approximate COUNT(DISTINCT)"
onChange={() =>
- changeQueryContext(
- changeUseApproximateCountDistinct(
- queryContext,
- !useApproximateCountDistinct,
- ),
- )
+ changeQueryContext({
+ ...queryContext,
+ useApproximateCountDistinct:
!useApproximateCountDistinct,
+ })
}
/>
)}
@@ -519,8 +542,9 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
>
<Button
text={`Engine: ${
- (enginesLabelFn ? enginesLabelFn(queryEngine).text :
queryEngine) ||
- `auto (${enginesLabelFn ? enginesLabelFn(effectiveEngine) :
effectiveEngine})`
+ queryEngine
+ ? enginesLabelFn(queryEngine).text
+ : `${autoEngineLabel.text}
(${enginesLabelFn(effectiveEngine).text})`
}`}
rightIcon={IconNames.CARET_DOWN}
intent={intent}
@@ -531,8 +555,10 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
clusterCapacity={clusterCapacity}
queryContext={queryContext}
changeQueryContext={changeQueryContext}
- menuHeader={maxTaskMenuHeader}
- maxNumLabelFn={maxTaskLabelFn}
+ defaultQueryContext={defaultQueryContext}
+ menuHeader={maxTasksMenuHeader}
+ maxTasksLabelFn={maxTasksLabelFn}
+ fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
/>
)}
{ingestMode && (
@@ -544,8 +570,16 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
<MenuItem
key={i}
icon={tickIcon(m === arrayIngestMode)}
- text={m ? ARRAY_INGEST_MODE_DESCRIPTION[m] : '(server
default)'}
- onClick={() =>
changeQueryContext(changeArrayIngestMode(queryContext, m))}
+ text={
+ m
+ ? ARRAY_INGEST_MODE_DESCRIPTION[m]
+ : `(server default${
+ defaultQueryContext.arrayIngestMode
+ ? `: ${defaultQueryContext.arrayIngestMode}`
+ : ''
+ })`
+ }
+ onClick={() => changeQueryContext({ ...queryContext,
arrayIngestMode: m })}
/>
))}
<MenuDivider />
@@ -594,7 +628,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
title="Custom timezone"
placeholder="Etc/UTC"
maxLength={50}
- onSubmit={tz => changeQueryContext(changeTimezone(queryContext, tz))}
+ onSubmit={sqlTimeZone => changeQueryContext({ ...queryContext,
sqlTimeZone })}
onClose={() => setCustomTimezoneDialogOpen(false)}
/>
)}
diff --git a/web-console/src/views/workbench-view/workbench-view.tsx
b/web-console/src/views/workbench-view/workbench-view.tsx
index aaac54e6d4a..e4f15a3aa18 100644
--- a/web-console/src/views/workbench-view/workbench-view.tsx
+++ b/web-console/src/views/workbench-view/workbench-view.tsx
@@ -30,7 +30,6 @@ import type { SqlQuery } from '@druid-toolkit/query';
import { SqlExpression } from '@druid-toolkit/query';
import classNames from 'classnames';
import copy from 'copy-to-clipboard';
-import type { ComponentProps } from 'react';
import React from 'react';
import { SpecDialog, StringInputDialog } from '../../dialogs';
@@ -38,10 +37,15 @@ import type {
CapacityInfo,
DruidEngine,
Execution,
+ QueryContext,
QueryWithContext,
TabEntry,
} from '../../druid-models';
-import { guessDataSourceNameFromInputSource, WorkbenchQuery } from
'../../druid-models';
+import {
+ DEFAULT_SERVER_QUERY_CONTEXT,
+ guessDataSourceNameFromInputSource,
+ WorkbenchQuery,
+} from '../../druid-models';
import type { Capabilities } from '../../helpers';
import { convertSpecToSql, getSpecDatasourceName, getTaskExecution } from
'../../helpers';
import { getLink } from '../../links';
@@ -71,6 +75,7 @@ import type { ExecutionDetailsTab } from
'./execution-details-pane/execution-det
import { ExecutionSubmitDialog } from
'./execution-submit-dialog/execution-submit-dialog';
import { ExplainDialog } from './explain-dialog/explain-dialog';
import { MetadataChangeDetector } from './metadata-change-detector';
+import type { QueryTabProps } from './query-tab/query-tab';
import { QueryTab } from './query-tab/query-tab';
import { RecentQueryTaskPanel } from
'./recent-query-task-panel/recent-query-task-panel';
import { TabRenameDialog } from './tab-rename-dialog/tab-rename-dialog';
@@ -91,20 +96,23 @@ function externalDataTabId(tabId: string | undefined):
boolean {
return String(tabId).startsWith('connect-external-data');
}
-export interface WorkbenchViewProps {
+export interface WorkbenchViewProps
+ extends Pick<
+ QueryTabProps,
+ 'maxTasksMenuHeader' | 'enginesLabelFn' | 'maxTasksLabelFn' |
'fullClusterCapacityLabelFn'
+ > {
capabilities: Capabilities;
tabId: string | undefined;
onTabChange(newTabId: string): void;
initQueryWithContext: QueryWithContext | undefined;
- defaultQueryContext?: Record<string, any>;
- mandatoryQueryContext?: Record<string, any>;
+ baseQueryContext?: QueryContext;
+ defaultQueryContext?: QueryContext;
+ mandatoryQueryContext?: QueryContext;
+ serverQueryContext?: QueryContext;
queryEngines: DruidEngine[];
allowExplain: boolean;
goToTask(taskId: string): void;
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
- maxTaskMenuHeader?: JSX.Element;
- enginesLabelFn?: ComponentProps<typeof QueryTab>['enginesLabelFn'];
- maxTaskLabelFn?: ComponentProps<typeof QueryTab>['maxTaskLabelFn'];
hideToolbar?: boolean;
}
@@ -249,11 +257,15 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
});
};
+ private getInitWorkbenchQuery(): WorkbenchQuery {
+ return
WorkbenchQuery.blank().changeQueryContext(this.props.defaultQueryContext || {});
+ }
+
private getInitTab(): TabEntry {
return {
id: generate8HexId(),
tabName: 'Tab 1',
- query:
WorkbenchQuery.blank().changeQueryContext(this.props.defaultQueryContext || {}),
+ query: this.getInitWorkbenchQuery(),
};
}
@@ -607,7 +619,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
icon={IconNames.PLUS}
minimal
onClick={() => {
- this.handleNewTab(WorkbenchQuery.blank());
+ this.handleNewTab(this.getInitWorkbenchQuery());
}}
/>
</div>
@@ -651,13 +663,16 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
const {
capabilities,
mandatoryQueryContext,
+ baseQueryContext,
+ serverQueryContext = DEFAULT_SERVER_QUERY_CONTEXT,
queryEngines,
allowExplain,
goToTask,
getClusterCapacity,
- maxTaskMenuHeader,
+ maxTasksMenuHeader,
enginesLabelFn,
- maxTaskLabelFn,
+ maxTasksLabelFn,
+ fullClusterCapacityLabelFn,
} = this.props;
const { columnMetadataState } = this.state;
const currentTabEntry = this.getCurrentTabEntry();
@@ -674,6 +689,8 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
query={currentTabEntry.query}
id={currentTabEntry.id}
mandatoryQueryContext={mandatoryQueryContext}
+ baseQueryContext={baseQueryContext}
+ serverQueryContext={serverQueryContext}
columnMetadata={columnMetadataState.getSomeData()}
onQueryChange={this.handleQueryChange}
onQueryTab={this.handleNewTab}
@@ -682,9 +699,10 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
clusterCapacity={capabilities.getMaxTaskSlots()}
goToTask={goToTask}
getClusterCapacity={getClusterCapacity}
- maxTaskMenuHeader={maxTaskMenuHeader}
+ maxTasksMenuHeader={maxTasksMenuHeader}
enginesLabelFn={enginesLabelFn}
- maxTaskLabelFn={maxTaskLabelFn}
+ maxTasksLabelFn={maxTasksLabelFn}
+ fullClusterCapacityLabelFn={fullClusterCapacityLabelFn}
runMoreMenu={
<Menu>
{allowExplain &&
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]