This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch 25.0.0
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/25.0.0 by this push:
new ef34c6b432 Web console: backport fixes to 25.0 (#13449)
ef34c6b432 is described below
commit ef34c6b432b9cfb466b9bb94d88dfe2796600fc6
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Wed Nov 30 23:05:04 2022 -0800
Web console: backport fixes to 25.0 (#13449)
* add ability to make inputFormat part of the example datasets (#13402)
* Web console: Index spec dialog (#13425)
* add index spec dialog
* add sanpshot
* Web console: be more robust to aux queries failing and improve kill tasks
(#13431)
* be more robust to aux queries failing
* feedback fixes
* remove empty block
* fix spelling
* remove killAllDataSources from the console
* don't render duration if aggregated (#13455)
---
.../src/components/auto-form/auto-form.scss | 12 +-
web-console/src/components/auto-form/auto-form.tsx | 55 ++-
.../form-group-with-info/form-group-with-info.scss | 14 +
.../table-clickable-cell/table-clickable-cell.scss | 4 +
.../table-clickable-cell/table-clickable-cell.tsx | 11 +-
.../warning-checklist/warning-checklist.tsx | 18 +-
.../async-action-dialog/async-action-dialog.tsx | 2 +-
.../__snapshots__/index-spec-dialog.spec.tsx.snap | 317 +++++++++++++++++
.../index-spec-dialog/index-spec-dialog.scss} | 21 +-
.../index-spec-dialog/index-spec-dialog.spec.tsx} | 37 +-
.../index-spec-dialog/index-spec-dialog.tsx | 88 +++++
web-console/src/dialogs/index.ts | 1 +
.../kill-datasource-dialog.tsx | 110 ++++++
.../compaction-status/compaction-status.spec.ts | 41 ++-
.../compaction-status/compaction-status.ts | 22 +-
.../coordinator-dynamic-config.tsx | 13 +-
.../src/druid-models/index-spec/index-spec.tsx | 158 +++++++++
web-console/src/druid-models/index.ts | 1 +
.../druid-models/ingestion-spec/ingestion-spec.tsx | 120 ++-----
.../workbench-query/workbench-query-part.ts | 4 +-
.../workbench-query/workbench-query.spec.ts | 2 +-
web-console/src/helpers/spec-conversion.spec.ts | 6 +
web-console/src/helpers/spec-conversion.ts | 5 +
.../views/datasources-view/datasources-view.tsx | 390 +++++++++++----------
.../src/views/ingestion-view/ingestion-view.tsx | 3 +-
.../src/views/services-view/services-view.tsx | 215 +++++++-----
.../input-source-step/example-inputs.ts | 67 +++-
.../input-source-step/input-source-step.tsx | 36 +-
.../views/workbench-view/run-panel/run-panel.tsx | 130 ++++---
29 files changed, 1364 insertions(+), 539 deletions(-)
diff --git a/web-console/src/components/auto-form/auto-form.scss
b/web-console/src/components/auto-form/auto-form.scss
index 5523f0f817..c303abc294 100644
--- a/web-console/src/components/auto-form/auto-form.scss
+++ b/web-console/src/components/auto-form/auto-form.scss
@@ -16,16 +16,8 @@
* limitations under the License.
*/
-@import '../../variables';
-
.auto-form {
- // Popover in info label
- label.#{$bp-ns}-label {
- position: relative;
-
- .#{$bp-ns}-text-muted {
- position: absolute;
- right: 0;
- }
+ .custom-input input {
+ cursor: pointer;
}
}
diff --git a/web-console/src/components/auto-form/auto-form.tsx
b/web-console/src/components/auto-form/auto-form.tsx
index 1e56ef2b72..146de61b62 100644
--- a/web-console/src/components/auto-form/auto-form.tsx
+++ b/web-console/src/components/auto-form/auto-form.tsx
@@ -16,7 +16,14 @@
* limitations under the License.
*/
-import { Button, ButtonGroup, FormGroup, Intent, NumericInput } from
'@blueprintjs/core';
+import {
+ Button,
+ ButtonGroup,
+ FormGroup,
+ InputGroup,
+ Intent,
+ NumericInput,
+} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import React from 'react';
@@ -46,7 +53,8 @@ export interface Field<M> {
| 'boolean'
| 'string-array'
| 'json'
- | 'interval';
+ | 'interval'
+ | 'custom';
defaultValue?: any;
emptyValue?: any;
suggestions?: Functor<M, Suggestion[]>;
@@ -64,6 +72,13 @@ export interface Field<M> {
valueAdjustment?: (value: any) => any;
adjustment?: (model: Partial<M>) => Partial<M>;
issueWithValue?: (value: any) => string | undefined;
+
+ customSummary?: (v: any) => string;
+ customDialog?: (o: {
+ value: any;
+ onValueChange: (v: any) => void;
+ onClose: () => void;
+ }) => JSX.Element;
}
interface ComputedFieldValues {
@@ -84,6 +99,7 @@ export interface AutoFormProps<M> {
export interface AutoFormState {
showMore: boolean;
+ customDialog?: JSX.Element;
}
export class AutoForm<T extends Record<string, any>> extends
React.PureComponent<
@@ -395,6 +411,36 @@ export class AutoForm<T extends Record<string, any>>
extends React.PureComponent
);
}
+ private renderCustomInput(field: Field<T>): JSX.Element {
+ const { model } = this.props;
+ const { required, defaultValue, modelValue } =
AutoForm.computeFieldValues(model, field);
+ const effectiveValue = modelValue || defaultValue;
+
+ const onEdit = () => {
+ this.setState({
+ customDialog: field.customDialog?.({
+ value: effectiveValue,
+ onValueChange: v => this.fieldChange(field, v),
+ onClose: () => {
+ this.setState({ customDialog: undefined });
+ },
+ }),
+ });
+ };
+
+ return (
+ <InputGroup
+ className="custom-input"
+ value={(field.customSummary || String)(effectiveValue)}
+ intent={required && modelValue == null ? AutoForm.REQUIRED_INTENT :
undefined}
+ readOnly
+ placeholder={AutoForm.evaluateFunctor(field.placeholder, model, '')}
+ rightElement={<Button icon={IconNames.EDIT} minimal onClick={onEdit}
/>}
+ onClick={onEdit}
+ />
+ );
+ }
+
renderFieldInput(field: Field<T>) {
switch (field.type) {
case 'number':
@@ -413,6 +459,8 @@ export class AutoForm<T extends Record<string, any>>
extends React.PureComponent
return this.renderJsonInput(field);
case 'interval':
return this.renderIntervalInput(field);
+ case 'custom':
+ return this.renderCustomInput(field);
default:
throw new Error(`unknown field type '${field.type}'`);
}
@@ -464,7 +512,7 @@ export class AutoForm<T extends Record<string, any>>
extends React.PureComponent
render(): JSX.Element {
const { fields, model, showCustom } = this.props;
- const { showMore } = this.state;
+ const { showMore, customDialog } = this.state;
let shouldShowMore = false;
const shownFields = fields.filter(field => {
@@ -489,6 +537,7 @@ export class AutoForm<T extends Record<string, any>>
extends React.PureComponent
{model && shownFields.map(this.renderField)}
{model && showCustom && showCustom(model) && this.renderCustom()}
{shouldShowMore && this.renderMoreOrLess()}
+ {customDialog}
</div>
);
}
diff --git
a/web-console/src/components/form-group-with-info/form-group-with-info.scss
b/web-console/src/components/form-group-with-info/form-group-with-info.scss
index c9587cb088..a64c6d2927 100644
--- a/web-console/src/components/form-group-with-info/form-group-with-info.scss
+++ b/web-console/src/components/form-group-with-info/form-group-with-info.scss
@@ -19,6 +19,20 @@
@import '../../variables';
.form-group-with-info {
+ label.#{$bp-ns}-label {
+ position: relative;
+
+ .#{$bp-ns}-text-muted {
+ position: absolute;
+ right: 0;
+
+ // This is only needed because BP4 alerts are too agro in setting CSS on
icons
+ .#{$bp-ns}-icon {
+ margin-right: 0;
+ }
+ }
+ }
+
.#{$bp-ns}-text-muted .#{$bp-ns}-popover2-target {
margin-top: 0;
}
diff --git
a/web-console/src/components/table-clickable-cell/table-clickable-cell.scss
b/web-console/src/components/table-clickable-cell/table-clickable-cell.scss
index d6f6f8b2d7..5c5991df54 100644
--- a/web-console/src/components/table-clickable-cell/table-clickable-cell.scss
+++ b/web-console/src/components/table-clickable-cell/table-clickable-cell.scss
@@ -24,6 +24,10 @@
overflow: hidden;
text-overflow: ellipsis;
+ &.disabled {
+ cursor: not-allowed;
+ }
+
.hover-icon {
position: absolute;
top: $table-cell-v-padding;
diff --git
a/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
b/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
index cc8cfd71e5..7e4c66fdd5 100644
--- a/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
+++ b/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
@@ -27,18 +27,23 @@ export interface TableClickableCellProps {
onClick: MouseEventHandler<any>;
hoverIcon?: IconName;
title?: string;
+ disabled?: boolean;
children?: ReactNode;
}
export const TableClickableCell = React.memo(function TableClickableCell(
props: TableClickableCellProps,
) {
- const { className, onClick, hoverIcon, title, children } = props;
+ const { className, onClick, hoverIcon, title, disabled, children } = props;
return (
- <div className={classNames('table-clickable-cell', className)}
title={title} onClick={onClick}>
+ <div
+ className={classNames('table-clickable-cell', className, { disabled })}
+ title={title}
+ onClick={disabled ? undefined : onClick}
+ >
{children}
- {hoverIcon && <Icon className="hover-icon" icon={hoverIcon} />}
+ {hoverIcon && !disabled && <Icon className="hover-icon" icon={hoverIcon}
/>}
</div>
);
});
diff --git a/web-console/src/components/warning-checklist/warning-checklist.tsx
b/web-console/src/components/warning-checklist/warning-checklist.tsx
index 449ad97004..5c74cbdb08 100644
--- a/web-console/src/components/warning-checklist/warning-checklist.tsx
+++ b/web-console/src/components/warning-checklist/warning-checklist.tsx
@@ -17,29 +17,31 @@
*/
import { Switch } from '@blueprintjs/core';
-import React, { useState } from 'react';
+import React, { ReactNode, useState } from 'react';
export interface WarningChecklistProps {
- checks: string[];
- onChange: (allChecked: boolean) => void;
+ checks: ReactNode[];
+ onChange(allChecked: boolean): void;
}
export const WarningChecklist = React.memo(function WarningChecklist(props:
WarningChecklistProps) {
const { checks, onChange } = props;
- const [checked, setChecked] = useState<Record<string, boolean>>({});
+ const [checked, setChecked] = useState<Record<number, boolean>>({});
- function doCheck(check: string) {
+ function doCheck(checkIndex: number) {
const newChecked = { ...checked };
- newChecked[check] = !newChecked[check];
+ newChecked[checkIndex] = !newChecked[checkIndex];
setChecked(newChecked);
- onChange(checks.every(check => newChecked[check]));
+ onChange(checks.every((_, i) => newChecked[i]));
}
return (
<div className="warning-checklist">
{checks.map((check, i) => (
- <Switch key={i} label={check} onChange={() => doCheck(check)} />
+ <Switch key={i} onChange={() => doCheck(i)}>
+ {check}
+ </Switch>
))}
</div>
);
diff --git
a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
index f892936bab..0d8cf385a5 100644
--- a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
+++ b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
@@ -47,7 +47,7 @@ export interface AsyncActionDialogProps {
intent?: Intent;
successText: string;
failText: string;
- warningChecks?: string[];
+ warningChecks?: ReactNode[];
children?: ReactNode;
}
diff --git
a/web-console/src/dialogs/index-spec-dialog/__snapshots__/index-spec-dialog.spec.tsx.snap
b/web-console/src/dialogs/index-spec-dialog/__snapshots__/index-spec-dialog.spec.tsx.snap
new file mode 100644
index 0000000000..57d989621b
--- /dev/null
+++
b/web-console/src/dialogs/index-spec-dialog/__snapshots__/index-spec-dialog.spec.tsx.snap
@@ -0,0 +1,317 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IndexSpecDialog matches snapshot with indexSpec 1`] = `
+<Blueprint4.Dialog
+ canOutsideClickClose={false}
+ className="index-spec-dialog"
+ isOpen={true}
+ onClose={[Function]}
+ title="Index spec"
+>
+ <Memo(FormJsonSelector)
+ onChange={[Function]}
+ tab="form"
+ />
+ <div
+ className="content"
+ >
+ <AutoForm
+ fields={
+ Array [
+ Object {
+ "defaultValue": "utf8",
+ "info": <React.Fragment>
+ Encoding format for STRING value dictionaries used by STRING and
COMPLEX<json> columns.
+ </React.Fragment>,
+ "label": "String dictionary encoding",
+ "name": "stringDictionaryEncoding.type",
+ "suggestions": Array [
+ "utf8",
+ "frontCoded",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": 4,
+ "defined": [Function],
+ "info": <React.Fragment>
+ The number of values to place in a bucket to perform delta
encoding. Must be a power of 2, maximum is 128.
+ </React.Fragment>,
+ "label": "String dictionary encoding bucket size",
+ "max": 128,
+ "min": 1,
+ "name": "stringDictionaryEncoding.bucketSize",
+ "type": "number",
+ },
+ Object {
+ "defaultValue": "roaring",
+ "info": <React.Fragment>
+ Compression format for bitmap indexes.
+ </React.Fragment>,
+ "label": "Bitmap type",
+ "name": "bitmap.type",
+ "suggestions": Array [
+ "roaring",
+ "concise",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": true,
+ "defined": [Function],
+ "info": <React.Fragment>
+ Controls whether or not run-length encoding will be used when it
is determined to be more space-efficient.
+ </React.Fragment>,
+ "label": "Bitmap compress run on serialization",
+ "name": "bitmap.compressRunOnSerialization",
+ "type": "boolean",
+ },
+ Object {
+ "defaultValue": "lz4",
+ "info": <React.Fragment>
+ Compression format for dimension columns.
+ </React.Fragment>,
+ "name": "dimensionCompression",
+ "suggestions": Array [
+ "lz4",
+ "lzf",
+ "zstd",
+ "uncompressed",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": "longs",
+ "info": <React.Fragment>
+ Encoding format for long-typed columns. Applies regardless of
whether they are dimensions or metrics.
+ <Unknown>
+ auto
+ </Unknown>
+ encodes the values using offset or lookup table depending on
column cardinality, and store them with variable size.
+ <Unknown>
+ longs
+ </Unknown>
+ stores the value as-is with 8 bytes each.
+ </React.Fragment>,
+ "name": "longEncoding",
+ "suggestions": Array [
+ "longs",
+ "auto",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": "lz4",
+ "info": <React.Fragment>
+ Compression format for primitive type metric columns.
+ </React.Fragment>,
+ "name": "metricCompression",
+ "suggestions": Array [
+ "lz4",
+ "lzf",
+ "zstd",
+ "uncompressed",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": "lz4",
+ "info": <React.Fragment>
+ Compression format to use for nested column raw data.
+ </React.Fragment>,
+ "label": "JSON compression",
+ "name": "jsonCompression",
+ "suggestions": Array [
+ "lz4",
+ "lzf",
+ "zstd",
+ "uncompressed",
+ ],
+ "type": "string",
+ },
+ ]
+ }
+ model={
+ Object {
+ "dimensionCompression": "lzf",
+ }
+ }
+ onChange={[Function]}
+ />
+ </div>
+ <div
+ className="bp4-dialog-footer"
+ >
+ <div
+ className="bp4-dialog-footer-actions"
+ >
+ <Blueprint4.Button
+ onClick={[Function]}
+ text="Close"
+ />
+ <Blueprint4.Button
+ disabled={false}
+ intent="primary"
+ onClick={[Function]}
+ text="Save"
+ />
+ </div>
+ </div>
+</Blueprint4.Dialog>
+`;
+
+exports[`IndexSpecDialog matches snapshot without compactionConfig 1`] = `
+<Blueprint4.Dialog
+ canOutsideClickClose={false}
+ className="index-spec-dialog"
+ isOpen={true}
+ onClose={[Function]}
+ title="Index spec"
+>
+ <Memo(FormJsonSelector)
+ onChange={[Function]}
+ tab="form"
+ />
+ <div
+ className="content"
+ >
+ <AutoForm
+ fields={
+ Array [
+ Object {
+ "defaultValue": "utf8",
+ "info": <React.Fragment>
+ Encoding format for STRING value dictionaries used by STRING and
COMPLEX<json> columns.
+ </React.Fragment>,
+ "label": "String dictionary encoding",
+ "name": "stringDictionaryEncoding.type",
+ "suggestions": Array [
+ "utf8",
+ "frontCoded",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": 4,
+ "defined": [Function],
+ "info": <React.Fragment>
+ The number of values to place in a bucket to perform delta
encoding. Must be a power of 2, maximum is 128.
+ </React.Fragment>,
+ "label": "String dictionary encoding bucket size",
+ "max": 128,
+ "min": 1,
+ "name": "stringDictionaryEncoding.bucketSize",
+ "type": "number",
+ },
+ Object {
+ "defaultValue": "roaring",
+ "info": <React.Fragment>
+ Compression format for bitmap indexes.
+ </React.Fragment>,
+ "label": "Bitmap type",
+ "name": "bitmap.type",
+ "suggestions": Array [
+ "roaring",
+ "concise",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": true,
+ "defined": [Function],
+ "info": <React.Fragment>
+ Controls whether or not run-length encoding will be used when it
is determined to be more space-efficient.
+ </React.Fragment>,
+ "label": "Bitmap compress run on serialization",
+ "name": "bitmap.compressRunOnSerialization",
+ "type": "boolean",
+ },
+ Object {
+ "defaultValue": "lz4",
+ "info": <React.Fragment>
+ Compression format for dimension columns.
+ </React.Fragment>,
+ "name": "dimensionCompression",
+ "suggestions": Array [
+ "lz4",
+ "lzf",
+ "zstd",
+ "uncompressed",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": "longs",
+ "info": <React.Fragment>
+ Encoding format for long-typed columns. Applies regardless of
whether they are dimensions or metrics.
+ <Unknown>
+ auto
+ </Unknown>
+ encodes the values using offset or lookup table depending on
column cardinality, and store them with variable size.
+ <Unknown>
+ longs
+ </Unknown>
+ stores the value as-is with 8 bytes each.
+ </React.Fragment>,
+ "name": "longEncoding",
+ "suggestions": Array [
+ "longs",
+ "auto",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": "lz4",
+ "info": <React.Fragment>
+ Compression format for primitive type metric columns.
+ </React.Fragment>,
+ "name": "metricCompression",
+ "suggestions": Array [
+ "lz4",
+ "lzf",
+ "zstd",
+ "uncompressed",
+ ],
+ "type": "string",
+ },
+ Object {
+ "defaultValue": "lz4",
+ "info": <React.Fragment>
+ Compression format to use for nested column raw data.
+ </React.Fragment>,
+ "label": "JSON compression",
+ "name": "jsonCompression",
+ "suggestions": Array [
+ "lz4",
+ "lzf",
+ "zstd",
+ "uncompressed",
+ ],
+ "type": "string",
+ },
+ ]
+ }
+ model={Object {}}
+ onChange={[Function]}
+ />
+ </div>
+ <div
+ className="bp4-dialog-footer"
+ >
+ <div
+ className="bp4-dialog-footer-actions"
+ >
+ <Blueprint4.Button
+ onClick={[Function]}
+ text="Close"
+ />
+ <Blueprint4.Button
+ disabled={false}
+ intent="primary"
+ onClick={[Function]}
+ text="Save"
+ />
+ </div>
+ </div>
+</Blueprint4.Dialog>
+`;
diff --git a/web-console/src/components/auto-form/auto-form.scss
b/web-console/src/dialogs/index-spec-dialog/index-spec-dialog.scss
similarity index 79%
copy from web-console/src/components/auto-form/auto-form.scss
copy to web-console/src/dialogs/index-spec-dialog/index-spec-dialog.scss
index 5523f0f817..e7cc53ee47 100644
--- a/web-console/src/components/auto-form/auto-form.scss
+++ b/web-console/src/dialogs/index-spec-dialog/index-spec-dialog.scss
@@ -18,14 +18,19 @@
@import '../../variables';
-.auto-form {
- // Popover in info label
- label.#{$bp-ns}-label {
- position: relative;
+.index-spec-dialog {
+ &.#{$bp-ns}-dialog {
+ height: 70vh;
+ }
+
+ .form-json-selector {
+ margin: 15px;
+ }
- .#{$bp-ns}-text-muted {
- position: absolute;
- right: 0;
- }
+ .content {
+ margin: 0 15px 10px 0;
+ padding: 0 5px 0 15px;
+ flex: 1;
+ overflow: auto;
}
}
diff --git
a/web-console/src/components/form-group-with-info/form-group-with-info.scss
b/web-console/src/dialogs/index-spec-dialog/index-spec-dialog.spec.tsx
similarity index 53%
copy from
web-console/src/components/form-group-with-info/form-group-with-info.scss
copy to web-console/src/dialogs/index-spec-dialog/index-spec-dialog.spec.tsx
index c9587cb088..68f7f56b88 100644
--- a/web-console/src/components/form-group-with-info/form-group-with-info.scss
+++ b/web-console/src/dialogs/index-spec-dialog/index-spec-dialog.spec.tsx
@@ -16,20 +16,29 @@
* limitations under the License.
*/
-@import '../../variables';
+import { shallow } from 'enzyme';
+import React from 'react';
-.form-group-with-info {
- .#{$bp-ns}-text-muted .#{$bp-ns}-popover2-target {
- margin-top: 0;
- }
+import { IndexSpecDialog } from './index-spec-dialog';
- .#{$bp-ns}-form-content {
- position: relative;
+describe('IndexSpecDialog', () => {
+ it('matches snapshot without compactionConfig', () => {
+ const compactionDialog = shallow(
+ <IndexSpecDialog onClose={() => {}} onSave={() => {}}
indexSpec={undefined} />,
+ );
+ expect(compactionDialog).toMatchSnapshot();
+ });
- & > .info-popover {
- position: absolute;
- right: 0;
- top: 5px;
- }
- }
-}
+ it('matches snapshot with indexSpec', () => {
+ const compactionDialog = shallow(
+ <IndexSpecDialog
+ onClose={() => {}}
+ onSave={() => {}}
+ indexSpec={{
+ dimensionCompression: 'lzf',
+ }}
+ />,
+ );
+ expect(compactionDialog).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/dialogs/index-spec-dialog/index-spec-dialog.tsx
b/web-console/src/dialogs/index-spec-dialog/index-spec-dialog.tsx
new file mode 100644
index 0000000000..4c870df45a
--- /dev/null
+++ b/web-console/src/dialogs/index-spec-dialog/index-spec-dialog.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 { Button, Classes, Dialog, Intent } from '@blueprintjs/core';
+import React, { useState } from 'react';
+
+import { AutoForm, FormJsonSelector, FormJsonTabs, JsonInput } from
'../../components';
+import { INDEX_SPEC_FIELDS, IndexSpec } from '../../druid-models';
+
+import './index-spec-dialog.scss';
+
+export interface IndexSpecDialogProps {
+ title?: string;
+ onClose: () => void;
+ onSave: (indexSpec: IndexSpec) => void;
+ indexSpec: IndexSpec | undefined;
+}
+
+export const IndexSpecDialog = React.memo(function IndexSpecDialog(props:
IndexSpecDialogProps) {
+ const { title, indexSpec, onSave, onClose } = props;
+
+ const [currentTab, setCurrentTab] = useState<FormJsonTabs>('form');
+ const [currentIndexSpec, setCurrentIndexSpec] =
useState<IndexSpec>(indexSpec || {});
+ const [jsonError, setJsonError] = useState<Error | undefined>();
+
+ const issueWithCurrentIndexSpec = AutoForm.issueWithModel(currentIndexSpec,
INDEX_SPEC_FIELDS);
+
+ return (
+ <Dialog
+ className="index-spec-dialog"
+ isOpen
+ onClose={onClose}
+ canOutsideClickClose={false}
+ title={title ?? 'Index spec'}
+ >
+ <FormJsonSelector tab={currentTab} onChange={setCurrentTab} />
+ <div className="content">
+ {currentTab === 'form' ? (
+ <AutoForm
+ fields={INDEX_SPEC_FIELDS}
+ model={currentIndexSpec}
+ onChange={m => setCurrentIndexSpec(m)}
+ />
+ ) : (
+ <JsonInput
+ value={currentIndexSpec}
+ onChange={v => {
+ setCurrentIndexSpec(v);
+ setJsonError(undefined);
+ }}
+ onError={setJsonError}
+ issueWithValue={value => AutoForm.issueWithModel(value,
INDEX_SPEC_FIELDS)}
+ height="100%"
+ />
+ )}
+ </div>
+ <div className={Classes.DIALOG_FOOTER}>
+ <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+ <Button text="Close" onClick={onClose} />
+ <Button
+ text="Save"
+ intent={Intent.PRIMARY}
+ disabled={Boolean(jsonError || issueWithCurrentIndexSpec)}
+ onClick={() => {
+ onSave(currentIndexSpec);
+ onClose();
+ }}
+ />
+ </div>
+ </div>
+ </Dialog>
+ );
+});
diff --git a/web-console/src/dialogs/index.ts b/web-console/src/dialogs/index.ts
index 9509442c8b..588257c84e 100644
--- a/web-console/src/dialogs/index.ts
+++ b/web-console/src/dialogs/index.ts
@@ -24,6 +24,7 @@ export * from './diff-dialog/diff-dialog';
export * from './doctor-dialog/doctor-dialog';
export * from './edit-context-dialog/edit-context-dialog';
export * from './history-dialog/history-dialog';
+export * from './kill-datasource-dialog/kill-datasource-dialog';
export * from './lookup-edit-dialog/lookup-edit-dialog';
export * from './numeric-input-dialog/numeric-input-dialog';
export * from
'./overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
diff --git
a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
new file mode 100644
index 0000000000..3eb7e9fdf2
--- /dev/null
+++ b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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 { Code, Intent } from '@blueprintjs/core';
+import React, { useState } from 'react';
+
+import { FormGroupWithInfo, PopoverText } from '../../components';
+import { SuggestibleInput } from
'../../components/suggestible-input/suggestible-input';
+import { Api } from '../../singletons';
+import { uniq } from '../../utils';
+import { AsyncActionDialog } from '../async-action-dialog/async-action-dialog';
+
+function getSuggestions(): string[] {
+ // Default to a data 24h ago so as not to cause a conflict between streaming
ingestion and kill tasks
+ const end = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
+ const startOfDay = end.slice(0, 10);
+ const startOfMonth = end.slice(0, 7) + '-01';
+ const startOfYear = end.slice(0, 4) + '-01-01';
+
+ return uniq([
+ `1000-01-01/${startOfDay}`,
+ `1000-01-01/${startOfMonth}`,
+ `1000-01-01/${startOfYear}`,
+ '1000-01-01/3000-01-01',
+ ]);
+}
+
+export interface KillDatasourceDialogProps {
+ datasource: string;
+ onClose(): void;
+ onSuccess(): void;
+}
+
+export const KillDatasourceDialog = function KillDatasourceDialog(
+ props: KillDatasourceDialogProps,
+) {
+ const { datasource, onClose, onSuccess } = props;
+ const suggestions = getSuggestions();
+ const [interval, setInterval] = useState<string>(suggestions[0]);
+
+ return (
+ <AsyncActionDialog
+ className="kill-datasource-dialog"
+ action={async () => {
+ const resp = await Api.instance.delete(
+ `/druid/coordinator/v1/datasources/${Api.encodePath(
+ datasource,
+ )}?kill=true&interval=${Api.encodePath(interval)}`,
+ {},
+ );
+ return resp.data;
+ }}
+ confirmButtonText="Permanently delete unused segments"
+ successText="Kill task was issued. Unused segments in datasource will be
deleted"
+ failText="Failed submit kill task"
+ intent={Intent.DANGER}
+ onClose={onClose}
+ onSuccess={onSuccess}
+ warningChecks={[
+ <>
+ I understand that this operation will delete all metadata about the
unused segments of{' '}
+ <Code>{datasource}</Code> and removes them from deep storage.
+ </>,
+ 'I understand that this operation cannot be undone.',
+ ]}
+ >
+ <p>
+ Are you sure you want to permanently delete unused segments in
<Code>{datasource}</Code>?
+ </p>
+ <p>This action is not reversible and the data deleted will be lost.</p>
+ <FormGroupWithInfo
+ label="Interval to delete"
+ info={
+ <PopoverText>
+ <p>
+ The range of time over which to delete unused segments specified
in ISO8601 interval
+ format.
+ </p>
+ <p>
+ If you have streaming ingestion running make sure that your
interval range doe not
+ overlap with intervals where streaming data is being added -
otherwise the kill task
+ will not start.
+ </p>
+ </PopoverText>
+ }
+ >
+ <SuggestibleInput
+ value={interval}
+ onValueChange={s => setInterval(s || '')}
+ suggestions={suggestions}
+ />
+ </FormGroupWithInfo>
+ </AsyncActionDialog>
+ );
+};
diff --git
a/web-console/src/druid-models/compaction-status/compaction-status.spec.ts
b/web-console/src/druid-models/compaction-status/compaction-status.spec.ts
index 8ed0c51413..9d1254090b 100644
--- a/web-console/src/druid-models/compaction-status/compaction-status.spec.ts
+++ b/web-console/src/druid-models/compaction-status/compaction-status.spec.ts
@@ -18,11 +18,7 @@
import { CompactionConfig } from '../compaction-config/compaction-config';
-import {
- CompactionStatus,
- formatCompactionConfigAndStatus,
- zeroCompactionStatus,
-} from './compaction-status';
+import { CompactionStatus, formatCompactionInfo, zeroCompactionStatus } from
'./compaction-status';
describe('compaction status', () => {
const BASIC_CONFIG: CompactionConfig = {};
@@ -61,27 +57,30 @@ describe('compaction status', () => {
});
it('formatCompactionConfigAndStatus', () => {
- expect(formatCompactionConfigAndStatus(undefined, undefined)).toEqual('Not
enabled');
+ expect(formatCompactionInfo({})).toEqual('Not enabled');
- expect(formatCompactionConfigAndStatus(BASIC_CONFIG,
undefined)).toEqual('Awaiting first run');
+ expect(formatCompactionInfo({ config: BASIC_CONFIG })).toEqual('Awaiting
first run');
- expect(formatCompactionConfigAndStatus(undefined,
ZERO_STATUS)).toEqual('Not enabled');
+ expect(formatCompactionInfo({ status: ZERO_STATUS })).toEqual('Not
enabled');
- expect(formatCompactionConfigAndStatus(BASIC_CONFIG,
ZERO_STATUS)).toEqual('Running');
+ expect(formatCompactionInfo({ config: BASIC_CONFIG, status: ZERO_STATUS
})).toEqual('Running');
expect(
- formatCompactionConfigAndStatus(BASIC_CONFIG, {
- dataSource: 'tbl',
- scheduleStatus: 'RUNNING',
- bytesAwaitingCompaction: 0,
- bytesCompacted: 100,
- bytesSkipped: 0,
- segmentCountAwaitingCompaction: 0,
- segmentCountCompacted: 10,
- segmentCountSkipped: 0,
- intervalCountAwaitingCompaction: 0,
- intervalCountCompacted: 10,
- intervalCountSkipped: 0,
+ formatCompactionInfo({
+ config: BASIC_CONFIG,
+ status: {
+ dataSource: 'tbl',
+ scheduleStatus: 'RUNNING',
+ bytesAwaitingCompaction: 0,
+ bytesCompacted: 100,
+ bytesSkipped: 0,
+ segmentCountAwaitingCompaction: 0,
+ segmentCountCompacted: 10,
+ segmentCountSkipped: 0,
+ intervalCountAwaitingCompaction: 0,
+ intervalCountCompacted: 10,
+ intervalCountSkipped: 0,
+ },
}),
).toEqual('Fully compacted');
});
diff --git
a/web-console/src/druid-models/compaction-status/compaction-status.ts
b/web-console/src/druid-models/compaction-status/compaction-status.ts
index 2982d9b69e..d17f2c44fd 100644
--- a/web-console/src/druid-models/compaction-status/compaction-status.ts
+++ b/web-console/src/druid-models/compaction-status/compaction-status.ts
@@ -50,19 +50,19 @@ export function zeroCompactionStatus(compactionStatus:
CompactionStatus): boolea
);
}
-export function formatCompactionConfigAndStatus(
- compactionConfig: CompactionConfig | undefined,
- compactionStatus: CompactionStatus | undefined,
-) {
- if (compactionConfig) {
- if (compactionStatus) {
- if (
- compactionStatus.bytesAwaitingCompaction === 0 &&
- !zeroCompactionStatus(compactionStatus)
- ) {
+export interface CompactionInfo {
+ config?: CompactionConfig;
+ status?: CompactionStatus;
+}
+
+export function formatCompactionInfo(compaction: CompactionInfo) {
+ const { config, status } = compaction;
+ if (config) {
+ if (status) {
+ if (status.bytesAwaitingCompaction === 0 &&
!zeroCompactionStatus(status)) {
return 'Fully compacted';
} else {
- return capitalizeFirst(compactionStatus.scheduleStatus);
+ return capitalizeFirst(status.scheduleStatus);
}
} else {
return 'Awaiting first run';
diff --git
a/web-console/src/druid-models/coordinator-dynamic-config/coordinator-dynamic-config.tsx
b/web-console/src/druid-models/coordinator-dynamic-config/coordinator-dynamic-config.tsx
index eeb25db09c..ca957309ff 100644
---
a/web-console/src/druid-models/coordinator-dynamic-config/coordinator-dynamic-config.tsx
+++
b/web-console/src/druid-models/coordinator-dynamic-config/coordinator-dynamic-config.tsx
@@ -69,20 +69,9 @@ export const COORDINATOR_DYNAMIC_CONFIG_FIELDS:
Field<CoordinatorDynamicConfig>[
</>
),
},
- {
- name: 'killAllDataSources',
- type: 'boolean',
- defaultValue: false,
- info: (
- <>
- Send kill tasks for ALL dataSources if property
<Code>druid.coordinator.kill.on</Code> is
- true. If this is set to true then <Code>killDataSourceWhitelist</Code>
must not be specified
- or be empty list.
- </>
- ),
- },
{
name: 'killDataSourceWhitelist',
+ label: 'Kill datasource whitelist',
type: 'string-array',
emptyValue: [],
info: (
diff --git a/web-console/src/druid-models/index-spec/index-spec.tsx
b/web-console/src/druid-models/index-spec/index-spec.tsx
new file mode 100644
index 0000000000..1a42462996
--- /dev/null
+++ b/web-console/src/druid-models/index-spec/index-spec.tsx
@@ -0,0 +1,158 @@
+/*
+ * 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 { Code } from '@blueprintjs/core';
+import React from 'react';
+
+import { Field } from '../../components';
+import { deepGet } from '../../utils';
+
+export interface IndexSpec {
+ bitmap?: Bitmap;
+ dimensionCompression?: string;
+ stringDictionaryEncoding?: { type: 'utf8' | 'frontCoded'; bucketSize: number
};
+ metricCompression?: string;
+ longEncoding?: string;
+ jsonCompression?: string;
+}
+
+export interface Bitmap {
+ type: string;
+ compressRunOnSerialization?: boolean;
+}
+
+export function summarizeIndexSpec(indexSpec: IndexSpec | undefined): string {
+ if (!indexSpec) return '';
+
+ const { stringDictionaryEncoding, bitmap, longEncoding } = indexSpec;
+
+ const ret: string[] = [];
+ if (stringDictionaryEncoding) {
+ switch (stringDictionaryEncoding.type) {
+ case 'frontCoded':
+ ret.push(`frontCoded(${stringDictionaryEncoding.bucketSize || 4})`);
+ break;
+
+ default:
+ ret.push(stringDictionaryEncoding.type);
+ break;
+ }
+ }
+
+ if (bitmap) {
+ ret.push(bitmap.type);
+ }
+
+ if (longEncoding) {
+ ret.push(longEncoding);
+ }
+
+ return ret.join('; ');
+}
+
+export const INDEX_SPEC_FIELDS: Field<IndexSpec>[] = [
+ {
+ name: 'stringDictionaryEncoding.type',
+ label: 'String dictionary encoding',
+ type: 'string',
+ defaultValue: 'utf8',
+ suggestions: ['utf8', 'frontCoded'],
+ info: (
+ <>
+ Encoding format for STRING value dictionaries used by STRING and
COMPLEX<json>
+ columns.
+ </>
+ ),
+ },
+ {
+ name: 'stringDictionaryEncoding.bucketSize',
+ label: 'String dictionary encoding bucket size',
+ type: 'number',
+ defaultValue: 4,
+ min: 1,
+ max: 128,
+ defined: spec => deepGet(spec, 'stringDictionaryEncoding.type') ===
'frontCoded',
+ info: (
+ <>
+ The number of values to place in a bucket to perform delta encoding.
Must be a power of 2,
+ maximum is 128.
+ </>
+ ),
+ },
+
+ {
+ name: 'bitmap.type',
+ label: 'Bitmap type',
+ type: 'string',
+ defaultValue: 'roaring',
+ suggestions: ['roaring', 'concise'],
+ info: <>Compression format for bitmap indexes.</>,
+ },
+ {
+ name: 'bitmap.compressRunOnSerialization',
+ label: 'Bitmap compress run on serialization',
+ type: 'boolean',
+ defaultValue: true,
+ defined: spec => (deepGet(spec, 'bitmap.type') || 'roaring') === 'roaring',
+ info: (
+ <>
+ Controls whether or not run-length encoding will be used when it is
determined to be more
+ space-efficient.
+ </>
+ ),
+ },
+
+ {
+ name: 'dimensionCompression',
+ type: 'string',
+ defaultValue: 'lz4',
+ suggestions: ['lz4', 'lzf', 'zstd', 'uncompressed'],
+ info: <>Compression format for dimension columns.</>,
+ },
+
+ {
+ name: 'longEncoding',
+ type: 'string',
+ defaultValue: 'longs',
+ suggestions: ['longs', 'auto'],
+ info: (
+ <>
+ Encoding format for long-typed columns. Applies regardless of whether
they are dimensions or
+ metrics. <Code>auto</Code> encodes the values using offset or lookup
table depending on
+ column cardinality, and store them with variable size.
<Code>longs</Code> stores the value
+ as-is with 8 bytes each.
+ </>
+ ),
+ },
+ {
+ name: 'metricCompression',
+ type: 'string',
+ defaultValue: 'lz4',
+ suggestions: ['lz4', 'lzf', 'zstd', 'uncompressed'],
+ info: <>Compression format for primitive type metric columns.</>,
+ },
+
+ {
+ name: 'jsonCompression',
+ label: 'JSON compression',
+ type: 'string',
+ defaultValue: 'lz4',
+ suggestions: ['lz4', 'lzf', 'zstd', 'uncompressed'],
+ info: <>Compression format to use for nested column raw data. </>,
+ },
+];
diff --git a/web-console/src/druid-models/index.ts
b/web-console/src/druid-models/index.ts
index 359ba70440..0b4ad6b65f 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -25,6 +25,7 @@ export * from './execution/execution';
export * from './external-config/external-config';
export * from './filter/filter';
export * from './flatten-spec/flatten-spec';
+export * from './index-spec/index-spec';
export * from './ingest-query-pattern/ingest-query-pattern';
export * from './ingestion-spec/ingestion-spec';
export * from './input-format/input-format';
diff --git a/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
b/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
index b68052abd4..f144f4c2ad 100644
--- a/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
+++ b/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
@@ -21,6 +21,7 @@ import { range } from 'd3-array';
import React from 'react';
import { AutoForm, ExternalLink, Field } from '../../components';
+import { IndexSpecDialog } from
'../../dialogs/index-spec-dialog/index-spec-dialog';
import { getLink } from '../../links';
import {
allowKeys,
@@ -44,6 +45,7 @@ import {
getDimensionSpecs,
getDimensionSpecType,
} from '../dimension-spec/dimension-spec';
+import { IndexSpec, summarizeIndexSpec } from '../index-spec/index-spec';
import { InputFormat, issueWithInputFormat } from
'../input-format/input-format';
import {
FILTER_SUGGESTIONS,
@@ -1379,6 +1381,7 @@ export interface TuningConfig {
partitionsSpec?: PartitionsSpec;
maxPendingPersists?: number;
indexSpec?: IndexSpec;
+ indexSpecForIntermediatePersists?: IndexSpec;
forceExtendableShardSpecs?: boolean;
forceGuaranteedRollup?: boolean;
reportParseExceptions?: boolean;
@@ -1869,103 +1872,38 @@ const TUNING_FORM_FIELDS: Field<IngestionSpec>[] = [
},
{
- name: 'spec.tuningConfig.indexSpec.bitmap.type',
- label: 'Index bitmap type',
- type: 'string',
- defaultValue: 'roaring',
- suggestions: ['concise', 'roaring'],
+ name: 'spec.tuningConfig.indexSpec',
+ type: 'custom',
hideInMore: true,
- info: <>Compression format for bitmap indexes.</>,
- },
- {
- name: 'spec.tuningConfig.indexSpec.bitmap.compressRunOnSerialization',
- type: 'boolean',
- defaultValue: true,
- defined: spec => deepGet(spec, 'spec.tuningConfig.indexSpec.bitmap.type')
=== 'roaring',
- info: (
- <>
- Controls whether or not run-length encoding will be used when it is
determined to be more
- space-efficient.
- </>
+ info: <>Defines segment storage format options to use at indexing time.</>,
+ placeholder: 'Default index spec',
+ customSummary: summarizeIndexSpec,
+ customDialog: ({ value, onValueChange, onClose }) => (
+ <IndexSpecDialog onClose={onClose} onSave={onValueChange}
indexSpec={value} />
),
},
-
- {
- name: 'spec.tuningConfig.indexSpec.dimensionCompression',
- label: 'Index dimension compression',
- type: 'string',
- defaultValue: 'lz4',
- suggestions: ['lz4', 'lzf', 'zstd', 'uncompressed'],
- hideInMore: true,
- info: <>Compression format for dimension columns.</>,
- },
-
{
- name: 'spec.tuningConfig.indexSpec.stringDictionaryEncoding.type',
- label: 'Index string dictionary encoding',
- type: 'string',
- defaultValue: 'utf8',
- suggestions: ['utf8', 'frontCoded'],
+ name: 'spec.tuningConfig.indexSpecForIntermediatePersists',
+ type: 'custom',
hideInMore: true,
info: (
<>
- Encoding format for STRING value dictionaries used by STRING and
COMPLEX<json>
- columns.
+ Defines segment storage format options to use at indexing time for
intermediate persisted
+ temporary segments.
</>
),
- },
- {
- name: 'spec.tuningConfig.indexSpec.stringDictionaryEncoding.bucketSize',
- label: 'Index string dictionary encoding bucket size',
- type: 'number',
- defaultValue: 4,
- min: 1,
- max: 128,
- defined: spec =>
- deepGet(spec,
'spec.tuningConfig.indexSpec.stringDictionaryEncoding.type') === 'frontCoded',
- hideInMore: true,
- info: (
- <>
- The number of values to place in a bucket to perform delta encoding.
Must be a power of 2,
- maximum is 128.
- </>
+ placeholder: 'Default index spec',
+ customSummary: summarizeIndexSpec,
+ customDialog: ({ value, onValueChange, onClose }) => (
+ <IndexSpecDialog
+ title="Index spec for intermediate persists"
+ onClose={onClose}
+ onSave={onValueChange}
+ indexSpec={value}
+ />
),
},
- {
- name: 'spec.tuningConfig.indexSpec.metricCompression',
- label: 'Index metric compression',
- type: 'string',
- defaultValue: 'lz4',
- suggestions: ['lz4', 'lzf', 'zstd', 'uncompressed'],
- hideInMore: true,
- info: <>Compression format for primitive type metric columns.</>,
- },
- {
- name: 'spec.tuningConfig.indexSpec.longEncoding',
- label: 'Index long encoding',
- type: 'string',
- defaultValue: 'longs',
- suggestions: ['longs', 'auto'],
- hideInMore: true,
- info: (
- <>
- Encoding format for long-typed columns. Applies regardless of whether
they are dimensions or
- metrics. <Code>auto</Code> encodes the values using offset or lookup
table depending on
- column cardinality, and store them with variable size.
<Code>longs</Code> stores the value
- as-is with 8 bytes each.
- </>
- ),
- },
- {
- name: 'spec.tuningConfig.indexSpec.jsonCompression',
- label: 'Index JSON compression',
- type: 'string',
- defaultValue: 'lz4',
- suggestions: ['lz4', 'lzf', 'zstd', 'uncompressed'],
- hideInMore: true,
- info: <>Compression format to use for nested column raw data. </>,
- },
{
name: 'spec.tuningConfig.splitHintSpec.maxSplitSize',
type: 'number',
@@ -2172,18 +2110,6 @@ export function getTuningFormFields() {
return TUNING_FORM_FIELDS;
}
-export interface IndexSpec {
- bitmap?: Bitmap;
- dimensionCompression?: string;
- metricCompression?: string;
- longEncoding?: string;
-}
-
-export interface Bitmap {
- type: string;
- compressRunOnSerialization?: boolean;
-}
-
// --------------
export function updateIngestionType(
diff --git
a/web-console/src/druid-models/workbench-query/workbench-query-part.ts
b/web-console/src/druid-models/workbench-query/workbench-query-part.ts
index 604cfb0132..5e4afb453f 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query-part.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query-part.ts
@@ -62,10 +62,10 @@ export class WorkbenchQueryPart {
static getIngestDatasourceFromQueryFragment(queryFragment: string): string |
undefined {
// Assuming the queryFragment is no parsable find the prefix that look
like:
// REPLACE<space>INTO<space><whatever><space>SELECT<space or EOF>
- const matchInsertReplaceIndex =
queryFragment.match(/(?:INSERT|REPLACE)\s+INTO/)?.index;
+ const matchInsertReplaceIndex =
queryFragment.match(/(?:INSERT|REPLACE)\s+INTO/i)?.index;
if (typeof matchInsertReplaceIndex !== 'number') return;
- const matchEnd = queryFragment.match(/\b(?:SELECT|WITH)\b|$/);
+ const matchEnd = queryFragment.match(/\b(?:SELECT|WITH)\b|$/i);
const fragmentQuery = SqlQuery.maybeParse(
queryFragment.substring(matchInsertReplaceIndex, matchEnd?.index) + '
SELECT * FROM t',
);
diff --git
a/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
b/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
index 5d7e261509..9af0fb2407 100644
--- a/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
+++ b/web-console/src/druid-models/workbench-query/workbench-query.spec.ts
@@ -465,7 +465,7 @@ describe('WorkbenchQuery', () => {
it('works with INSERT (unparsable)', () => {
const sql = sane`
-- Some comment
- INSERT INTO trips2
+ INSERT into trips2
SELECT
TIME_PARSE(pickup_datetime) AS __time,
*
diff --git a/web-console/src/helpers/spec-conversion.spec.ts
b/web-console/src/helpers/spec-conversion.spec.ts
index 0239da1854..2f6aa59f51 100644
--- a/web-console/src/helpers/spec-conversion.spec.ts
+++ b/web-console/src/helpers/spec-conversion.spec.ts
@@ -106,6 +106,9 @@ describe('spec conversion', () => {
partitionDimension: 'isRobot',
targetRowsPerSegment: 150000,
},
+ indexSpec: {
+ dimensionCompression: 'lzf',
+ },
forceGuaranteedRollup: true,
maxNumConcurrentSubTasks: 4,
maxParseExceptions: 3,
@@ -159,6 +162,9 @@ describe('spec conversion', () => {
maxParseExceptions: 3,
finalizeAggregations: false,
maxNumTasks: 5,
+ indexSpec: {
+ dimensionCompression: 'lzf',
+ },
});
});
diff --git a/web-console/src/helpers/spec-conversion.ts
b/web-console/src/helpers/spec-conversion.ts
index d35433917f..9b76e787dd 100644
--- a/web-console/src/helpers/spec-conversion.ts
+++ b/web-console/src/helpers/spec-conversion.ts
@@ -70,6 +70,11 @@ export function convertSpecToSql(spec: any):
QueryWithContext {
groupByEnableMultiValueUnnesting: false,
};
+ const indexSpec = deepGet(spec, 'spec.tuningConfig.indexSpec');
+ if (indexSpec) {
+ context.indexSpec = indexSpec;
+ }
+
const lines: string[] = [];
const rollup = deepGet(spec, 'spec.dataSchema.granularitySpec.rollup') ??
true;
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx
b/web-console/src/views/datasources-view/datasources-view.tsx
index 7f384fc8bc..c38a42b639 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -36,12 +36,18 @@ import {
TableColumnSelector,
ViewControlBar,
} from '../../components';
-import { AsyncActionDialog, CompactionDialog, RetentionDialog } from
'../../dialogs';
+import {
+ AsyncActionDialog,
+ CompactionDialog,
+ KillDatasourceDialog,
+ RetentionDialog,
+} from '../../dialogs';
import { DatasourceTableActionDialog } from
'../../dialogs/datasource-table-action-dialog/datasource-table-action-dialog';
import {
CompactionConfig,
+ CompactionInfo,
CompactionStatus,
- formatCompactionConfigAndStatus,
+ formatCompactionInfo,
QueryWithContext,
zeroCompactionStatus,
} from '../../druid-models';
@@ -208,9 +214,8 @@ function segmentGranularityCountsToRank(row:
DatasourceQueryResultRow): number {
}
interface Datasource extends DatasourceQueryResultRow {
- readonly rules: Rule[];
- readonly compactionConfig?: CompactionConfig;
- readonly compactionStatus?: CompactionStatus;
+ readonly rules?: Rule[];
+ readonly compaction?: CompactionInfo;
readonly unused?: boolean;
}
@@ -220,7 +225,7 @@ function makeUnusedDatasource(datasource: string):
Datasource {
interface DatasourcesAndDefaultRules {
readonly datasources: Datasource[];
- readonly defaultRules: Rule[];
+ readonly defaultRules?: Rule[];
}
interface RetentionDialogOpenOn {
@@ -433,43 +438,85 @@ ORDER BY 1`;
let unused: string[] = [];
if (showUnused) {
- const unusedResp = await Api.instance.get<string[]>(
- '/druid/coordinator/v1/metadata/datasources?includeUnused',
+ try {
+ unused = (
+ await Api.instance.get<string[]>(
+ '/druid/coordinator/v1/metadata/datasources?includeUnused',
+ )
+ ).data.filter(d => !seen[d]);
+ } catch {
+ AppToaster.show({
+ icon: IconNames.ERROR,
+ intent: Intent.DANGER,
+ message: 'Could not get the list of unused datasources',
+ });
+ }
+ }
+
+ let rules: Record<string, Rule[]> = {};
+ try {
+ rules = (await Api.instance.get<Record<string,
Rule[]>>('/druid/coordinator/v1/rules'))
+ .data;
+ } catch {
+ AppToaster.show({
+ icon: IconNames.ERROR,
+ intent: Intent.DANGER,
+ message: 'Could not get load rules',
+ });
+ }
+
+ let compactionConfigs: Record<string, CompactionConfig> | undefined;
+ try {
+ const compactionConfigsResp = await Api.instance.get<{
+ compactionConfigs: CompactionConfig[];
+ }>('/druid/coordinator/v1/config/compaction');
+ compactionConfigs = lookupBy(
+ compactionConfigsResp.data.compactionConfigs || [],
+ c => c.dataSource,
);
- unused = unusedResp.data.filter(d => !seen[d]);
+ } catch {
+ AppToaster.show({
+ icon: IconNames.ERROR,
+ intent: Intent.DANGER,
+ message: 'Could not get compaction configs',
+ });
}
- const rulesResp = await Api.instance.get<Record<string, Rule[]>>(
- '/druid/coordinator/v1/rules',
- );
- const rules = rulesResp.data;
-
- const compactionConfigsResp = await Api.instance.get<{
- compactionConfigs: CompactionConfig[];
- }>('/druid/coordinator/v1/config/compaction');
- const compactionConfigs = lookupBy(
- compactionConfigsResp.data.compactionConfigs || [],
- c => c.dataSource,
- );
-
- const compactionStatusesResp = await Api.instance.get<{ latestStatus:
CompactionStatus[] }>(
- '/druid/coordinator/v1/compaction/status',
- );
- const compactionStatuses = lookupBy(
- compactionStatusesResp.data.latestStatus || [],
- c => c.dataSource,
- );
+ let compactionStatuses: Record<string, CompactionStatus> | undefined;
+ if (compactionConfigs) {
+ // Don't bother getting the statuses if we can not even get the
configs
+ try {
+ const compactionStatusesResp = await Api.instance.get<{
+ latestStatus: CompactionStatus[];
+ }>('/druid/coordinator/v1/compaction/status');
+ compactionStatuses = lookupBy(
+ compactionStatusesResp.data.latestStatus || [],
+ c => c.dataSource,
+ );
+ } catch {
+ AppToaster.show({
+ icon: IconNames.ERROR,
+ intent: Intent.DANGER,
+ message: 'Could not get compaction statuses',
+ });
+ }
+ }
return {
datasources:
datasources.concat(unused.map(makeUnusedDatasource)).map(ds => {
return {
...ds,
- rules: rules[ds.datasource] || [],
- compactionConfig: compactionConfigs[ds.datasource],
- compactionStatus: compactionStatuses[ds.datasource],
+ rules: rules[ds.datasource],
+ compaction:
+ compactionConfigs && compactionStatuses
+ ? {
+ config: compactionConfigs[ds.datasource],
+ status: compactionStatuses[ds.datasource],
+ }
+ : undefined,
};
}),
- defaultRules: rules[DEFAULT_RULES_KEY] || [],
+ defaultRules: rules[DEFAULT_RULES_KEY],
};
},
onStateChange: datasourcesAndDefaultRulesState => {
@@ -633,36 +680,15 @@ ORDER BY 1`;
if (!killDatasource) return;
return (
- <AsyncActionDialog
- action={async () => {
- const resp = await Api.instance.delete(
- `/druid/coordinator/v1/datasources/${Api.encodePath(
- killDatasource,
- )}?kill=true&interval=1000/3000`,
- {},
- );
- return resp.data;
- }}
- confirmButtonText="Permanently delete unused segments"
- successText="Kill task was issued. Unused segments in datasource will
be deleted"
- failText="Failed submit kill task"
- intent={Intent.DANGER}
+ <KillDatasourceDialog
+ datasource={killDatasource}
onClose={() => {
this.setState({ killDatasource: undefined });
}}
onSuccess={() => {
this.fetchDatasourceData();
}}
- warningChecks={[
- `I understand that this operation will delete all metadata about the
unused segments of ${killDatasource} and removes them from deep storage.`,
- 'I understand that this operation cannot be undone.',
- ]}
- >
- <p>
- {`Are you sure you want to permanently delete unused segments in
'${killDatasource}'?`}
- </p>
- <p>This action is not reversible and the data deleted will be lost.</p>
- </AsyncActionDialog>
+ />
);
}
@@ -756,20 +782,20 @@ ORDER BY 1`;
this.setState({ retentionDialogOpenOn: undefined });
setTimeout(() => {
this.setState(state => {
- const datasourcesAndDefaultRules =
state.datasourcesAndDefaultRulesState.data;
- if (!datasourcesAndDefaultRules) return {};
+ const defaultRules =
state.datasourcesAndDefaultRulesState.data?.defaultRules;
+ if (!defaultRules) return {};
return {
retentionDialogOpenOn: {
datasource: '_default',
- rules: datasourcesAndDefaultRules.defaultRules,
+ rules: defaultRules,
},
};
});
}, 50);
};
- private readonly saveCompaction = async (compactionConfig: any) => {
+ private readonly saveCompaction = async (compactionConfig: CompactionConfig)
=> {
if (!compactionConfig) return;
try {
await Api.instance.post(`/druid/coordinator/v1/config/compaction`,
compactionConfig);
@@ -819,8 +845,8 @@ ORDER BY 1`;
getDatasourceActions(
datasource: string,
unused: boolean | undefined,
- rules: Rule[],
- compactionConfig: CompactionConfig | undefined,
+ rules: Rule[] | undefined,
+ compactionInfo: CompactionInfo | undefined,
): BasicAction[] {
const { goToQuery, goToTask, capabilities } = this.props;
@@ -863,82 +889,83 @@ ORDER BY 1`;
},
];
} else {
- return goToActions.concat([
- {
- icon: IconNames.AUTOMATIC_UPDATES,
- title: 'Edit retention rules',
- onAction: () => {
- this.setState({
- retentionDialogOpenOn: {
- datasource,
- rules,
- },
- });
+ return goToActions.concat(
+ compact([
+ {
+ icon: IconNames.AUTOMATIC_UPDATES,
+ title: 'Edit retention rules',
+ onAction: () => {
+ this.setState({
+ retentionDialogOpenOn: {
+ datasource,
+ rules: rules || [],
+ },
+ });
+ },
},
- },
- {
- icon: IconNames.REFRESH,
- title: 'Mark as used all segments (will lead to reapplying retention
rules)',
- onAction: () =>
- this.setState({
- datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn: datasource,
- }),
- },
- {
- icon: IconNames.COMPRESSED,
- title: 'Edit compaction configuration',
- onAction: () => {
- this.setState({
- compactionDialogOpenOn: {
- datasource,
- compactionConfig,
- },
- });
+ {
+ icon: IconNames.REFRESH,
+ title: 'Mark as used all segments (will lead to reapplying
retention rules)',
+ onAction: () =>
+ this.setState({
+ datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn: datasource,
+ }),
},
- },
- {
- icon: IconNames.EXPORT,
- title: 'Mark as used segments by interval',
-
- onAction: () =>
- this.setState({
- datasourceToMarkSegmentsByIntervalIn: datasource,
- useUnuseAction: 'use',
- }),
- },
- {
- icon: IconNames.IMPORT,
- title: 'Mark as unused segments by interval',
-
- onAction: () =>
- this.setState({
- datasourceToMarkSegmentsByIntervalIn: datasource,
- useUnuseAction: 'unuse',
- }),
- },
- {
- icon: IconNames.IMPORT,
- title: 'Mark as unused all segments',
- intent: Intent.DANGER,
- onAction: () => this.setState({
datasourceToMarkAsUnusedAllSegmentsIn: datasource }),
- },
- {
- icon: IconNames.TRASH,
- title: 'Delete unused segments (issue kill task)',
- intent: Intent.DANGER,
- onAction: () => this.setState({ killDatasource: datasource }),
- },
- ]);
+ compactionInfo
+ ? {
+ icon: IconNames.COMPRESSED,
+ title: 'Edit compaction configuration',
+ onAction: () => {
+ this.setState({
+ compactionDialogOpenOn: {
+ datasource,
+ compactionConfig: compactionInfo.config,
+ },
+ });
+ },
+ }
+ : undefined,
+ {
+ icon: IconNames.EXPORT,
+ title: 'Mark as used segments by interval',
+
+ onAction: () =>
+ this.setState({
+ datasourceToMarkSegmentsByIntervalIn: datasource,
+ useUnuseAction: 'use',
+ }),
+ },
+ {
+ icon: IconNames.IMPORT,
+ title: 'Mark as unused segments by interval',
+
+ onAction: () =>
+ this.setState({
+ datasourceToMarkSegmentsByIntervalIn: datasource,
+ useUnuseAction: 'unuse',
+ }),
+ },
+ {
+ icon: IconNames.IMPORT,
+ title: 'Mark as unused all segments',
+ intent: Intent.DANGER,
+ onAction: () => this.setState({
datasourceToMarkAsUnusedAllSegmentsIn: datasource }),
+ },
+ {
+ icon: IconNames.TRASH,
+ title: 'Delete unused segments (issue kill task)',
+ intent: Intent.DANGER,
+ onAction: () => this.setState({ killDatasource: datasource }),
+ },
+ ]),
+ );
}
}
private renderRetentionDialog(): JSX.Element | undefined {
const { retentionDialogOpenOn, tiersState, datasourcesAndDefaultRulesState
} = this.state;
- const { defaultRules } = datasourcesAndDefaultRulesState.data || {
- datasources: [],
- defaultRules: [],
- };
- if (!retentionDialogOpenOn) return;
+ const defaultRules = datasourcesAndDefaultRulesState.data?.defaultRules;
+ if (!retentionDialogOpenOn || !defaultRules) return;
return (
<RetentionDialog
@@ -969,11 +996,11 @@ ORDER BY 1`;
}
private onDetail(datasource: Datasource): void {
- const { unused, rules, compactionConfig } = datasource;
+ const { unused, rules, compaction } = datasource;
this.setState({
datasourceTableActionDialogId: datasource.datasource,
- actions: this.getDatasourceActions(datasource.datasource, unused, rules,
compactionConfig),
+ actions: this.getDatasourceActions(datasource.datasource, unused, rules,
compaction),
});
}
@@ -982,9 +1009,7 @@ ORDER BY 1`;
const { datasourcesAndDefaultRulesState, datasourceFilter, showUnused,
visibleColumns } =
this.state;
- let { datasources, defaultRules } = datasourcesAndDefaultRulesState.data
- ? datasourcesAndDefaultRulesState.data
- : { datasources: [], defaultRules: [] };
+ let { datasources, defaultRules } = datasourcesAndDefaultRulesState.data
|| { datasources: [] };
if (!showUnused) {
datasources = datasources.filter(d => !d.unused);
@@ -1009,8 +1034,8 @@ ORDER BY 1`;
const replicatedSizeValues = datasources.map(d =>
formatReplicatedSize(d.replicated_size));
const leftToBeCompactedValues = datasources.map(d =>
- d.compactionStatus
- ? formatLeftToBeCompacted(d.compactionStatus.bytesAwaitingCompaction)
+ d.compaction?.status
+ ? formatLeftToBeCompacted(d.compaction?.status.bytesAwaitingCompaction)
: '-',
);
@@ -1297,24 +1322,26 @@ ORDER BY 1`;
Header: 'Compaction',
show: capabilities.hasCoordinatorAccess() &&
visibleColumns.shown('Compaction'),
id: 'compactionStatus',
- accessor: row => Boolean(row.compactionStatus),
+ accessor: row => Boolean(row.compaction?.status),
filterable: false,
width: 150,
Cell: ({ original }) => {
- const { datasource, compactionConfig, compactionStatus } =
original as Datasource;
+ const { datasource, compaction } = original as Datasource;
return (
<TableClickableCell
- onClick={() =>
+ disabled={!compaction}
+ onClick={() => {
+ if (!compaction) return;
this.setState({
compactionDialogOpenOn: {
datasource,
- compactionConfig,
+ compactionConfig: compaction.config,
},
- })
- }
+ });
+ }}
hoverIcon={IconNames.EDIT}
>
- {formatCompactionConfigAndStatus(compactionConfig,
compactionStatus)}
+ {compaction ? formatCompactionInfo(compaction) : 'Could not
get compaction info'}
</TableClickableCell>
);
},
@@ -1324,17 +1351,22 @@ ORDER BY 1`;
show: capabilities.hasCoordinatorAccess() &&
visibleColumns.shown('% Compacted'),
id: 'percentCompacted',
width: 200,
- accessor: ({ compactionStatus }) =>
- compactionStatus && compactionStatus.bytesCompacted
- ? compactionStatus.bytesCompacted /
- (compactionStatus.bytesAwaitingCompaction +
compactionStatus.bytesCompacted)
- : 0,
+ accessor: ({ compaction }) => {
+ const status = compaction?.status;
+ return status?.bytesCompacted
+ ? status.bytesCompacted / (status.bytesAwaitingCompaction +
status.bytesCompacted)
+ : 0;
+ },
filterable: false,
className: 'padded',
Cell: ({ original }) => {
- const { compactionStatus } = original as Datasource;
+ const { compaction } = original as Datasource;
+ if (!compaction) {
+ return 'Could not get compaction info';
+ }
- if (!compactionStatus || zeroCompactionStatus(compactionStatus))
{
+ const { status } = compaction;
+ if (!status || zeroCompactionStatus(status)) {
return (
<>
<BracedText text="-" braces={PERCENT_BRACES} /> {' '}
@@ -1348,20 +1380,14 @@ ORDER BY 1`;
<>
<BracedText
text={formatPercent(
- progress(
- compactionStatus.bytesCompacted,
- compactionStatus.bytesAwaitingCompaction,
- ),
+ progress(status.bytesCompacted,
status.bytesAwaitingCompaction),
)}
braces={PERCENT_BRACES}
/>{' '}
{' '}
<BracedText
text={formatPercent(
- progress(
- compactionStatus.segmentCountCompacted,
- compactionStatus.segmentCountAwaitingCompaction,
- ),
+ progress(status.segmentCountCompacted,
status.segmentCountAwaitingCompaction),
)}
braces={PERCENT_BRACES}
/>{' '}
@@ -1369,8 +1395,8 @@ ORDER BY 1`;
<BracedText
text={formatPercent(
progress(
- compactionStatus.intervalCountCompacted,
- compactionStatus.intervalCountAwaitingCompaction,
+ status.intervalCountCompacted,
+ status.intervalCountAwaitingCompaction,
),
)}
braces={PERCENT_BRACES}
@@ -1385,20 +1411,26 @@ ORDER BY 1`;
capabilities.hasCoordinatorAccess() &&
visibleColumns.shown('Left to be compacted'),
id: 'leftToBeCompacted',
width: 100,
- accessor: ({ compactionStatus }) =>
- (compactionStatus && compactionStatus.bytesAwaitingCompaction)
|| 0,
+ accessor: ({ compaction }) => {
+ const status = compaction?.status;
+ return status?.bytesAwaitingCompaction || 0;
+ },
filterable: false,
className: 'padded',
Cell: ({ original }) => {
- const { compactionStatus } = original as Datasource;
+ const { compaction } = original as Datasource;
+ if (!compaction) {
+ return 'Could not get compaction info';
+ }
- if (!compactionStatus) {
+ const { status } = compaction;
+ if (!status) {
return <BracedText text="-" braces={leftToBeCompactedValues}
/>;
}
return (
<BracedText
-
text={formatLeftToBeCompacted(compactionStatus.bytesAwaitingCompaction)}
+
text={formatLeftToBeCompacted(status.bytesAwaitingCompaction)}
braces={leftToBeCompactedValues}
/>
);
@@ -1408,26 +1440,30 @@ ORDER BY 1`;
Header: 'Retention',
show: capabilities.hasCoordinatorAccess() &&
visibleColumns.shown('Retention'),
id: 'retention',
- accessor: row => row.rules.length,
+ accessor: row => row.rules?.length || 0,
filterable: false,
width: 200,
Cell: ({ original }) => {
const { datasource, rules } = original as Datasource;
return (
<TableClickableCell
- onClick={() =>
+ disabled={!defaultRules}
+ onClick={() => {
+ if (!defaultRules) return;
this.setState({
retentionDialogOpenOn: {
datasource,
- rules,
+ rules: rules || [],
},
- })
- }
+ });
+ }}
hoverIcon={IconNames.EDIT}
>
- {rules.length
+ {rules?.length
? DatasourcesView.formatRules(rules)
- : `Cluster default:
${DatasourcesView.formatRules(defaultRules)}`}
+ : defaultRules
+ ? `Cluster default:
${DatasourcesView.formatRules(defaultRules)}`
+ : 'Could not get default rules'}
</TableClickableCell>
);
},
@@ -1440,12 +1476,12 @@ ORDER BY 1`;
width: ACTION_COLUMN_WIDTH,
filterable: false,
Cell: ({ value: datasource, original }) => {
- const { unused, rules, compactionConfig } = original as
Datasource;
+ const { unused, rules, compaction } = original as Datasource;
const datasourceActions = this.getDatasourceActions(
datasource,
unused,
rules,
- compactionConfig,
+ compaction,
);
return (
<ActionCell
diff --git a/web-console/src/views/ingestion-view/ingestion-view.tsx
b/web-console/src/views/ingestion-view/ingestion-view.tsx
index e25d9be28d..e350ad4985 100644
--- a/web-console/src/views/ingestion-view/ingestion-view.tsx
+++ b/web-console/src/views/ingestion-view/ingestion-view.tsx
@@ -917,7 +917,8 @@ ORDER BY
width: 80,
filterable: false,
className: 'padded',
- Cell({ value, original }) {
+ Cell({ value, original, aggregated }) {
+ if (aggregated) return '';
if (value > 0) {
return formatDuration(value);
}
diff --git a/web-console/src/views/services-view/services-view.tsx
b/web-console/src/views/services-view/services-view.tsx
index aa2a934533..9dff93c8ab 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -36,11 +36,12 @@ import {
import { AsyncActionDialog } from '../../dialogs';
import { QueryWithContext } from '../../druid-models';
import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from
'../../react-table';
-import { Api } from '../../singletons';
+import { Api, AppToaster } from '../../singletons';
import {
Capabilities,
CapabilitiesMode,
deepGet,
+ filterMap,
formatBytes,
formatBytesCompact,
hasPopoverOpen,
@@ -117,7 +118,7 @@ export interface ServicesViewState {
visibleColumns: LocalStorageBackedVisibility;
}
-interface ServiceQueryResultRow {
+interface ServiceResultRow {
readonly service: string;
readonly service_type: string;
readonly tier: string;
@@ -127,16 +128,18 @@ interface ServiceQueryResultRow {
readonly max_size: NumberLike;
readonly plaintext_port: number;
readonly tls_port: number;
+ loadQueueInfo?: LoadQueueInfo;
+ workerInfo?: WorkerInfo;
}
-interface LoadQueueStatus {
+interface LoadQueueInfo {
readonly segmentsToDrop: NumberLike;
readonly segmentsToDropSize: NumberLike;
readonly segmentsToLoad: NumberLike;
readonly segmentsToLoadSize: NumberLike;
}
-interface MiddleManagerQueryResultRow {
+interface WorkerInfo {
readonly availabilityGroups: string[];
readonly blacklistedUntil: string | null;
readonly currCapacityUsed: NumberLike;
@@ -153,11 +156,6 @@ interface MiddleManagerQueryResultRow {
};
}
-interface ServiceResultRow
- extends ServiceQueryResultRow,
- Partial<LoadQueueStatus>,
- Partial<MiddleManagerQueryResultRow> {}
-
export class ServicesView extends React.PureComponent<ServicesViewProps,
ServicesViewState> {
private readonly serviceQueryManager: QueryManager<Capabilities,
ServiceResultRow[]>;
@@ -198,7 +196,7 @@ ORDER BY
) DESC,
"service" DESC`;
- static async getServices(): Promise<ServiceQueryResultRow[]> {
+ static async getServices(): Promise<ServiceResultRow[]> {
const allServiceResp = await
Api.instance.get('/druid/coordinator/v1/servers?simple');
const allServices = allServiceResp.data;
return allServices.map((s: any) => {
@@ -228,7 +226,7 @@ ORDER BY
this.serviceQueryManager = new QueryManager({
processQuery: async capabilities => {
- let services: ServiceQueryResultRow[];
+ let services: ServiceResultRow[];
if (capabilities.hasSql()) {
services = await queryDruidSql({ query: ServicesView.SERVICE_SQL });
} else if (capabilities.hasCoordinatorAccess()) {
@@ -238,50 +236,49 @@ ORDER BY
}
if (capabilities.hasCoordinatorAccess()) {
- const loadQueueResponse = await Api.instance.get(
- '/druid/coordinator/v1/loadqueue?simple',
- );
- const loadQueues: Record<string, LoadQueueStatus> =
loadQueueResponse.data;
- services = services.map(s => {
- const loadQueueInfo = loadQueues[s.service];
- if (loadQueueInfo) {
- s = { ...s, ...loadQueueInfo };
- }
- return s;
- });
+ try {
+ const loadQueueInfos = (
+ await Api.instance.get<Record<string, LoadQueueInfo>>(
+ '/druid/coordinator/v1/loadqueue?simple',
+ )
+ ).data;
+ services.forEach(s => {
+ s.loadQueueInfo = loadQueueInfos[s.service];
+ });
+ } catch {
+ AppToaster.show({
+ icon: IconNames.ERROR,
+ intent: Intent.DANGER,
+ message: 'There was an error getting the load queue info',
+ });
+ }
}
if (capabilities.hasOverlordAccess()) {
- let middleManagers: MiddleManagerQueryResultRow[];
try {
- const middleManagerResponse = await
Api.instance.get('/druid/indexer/v1/workers');
- middleManagers = middleManagerResponse.data;
+ const workerInfos = (await
Api.instance.get<WorkerInfo[]>('/druid/indexer/v1/workers'))
+ .data;
+
+ const workerInfoLookup: Record<string, WorkerInfo> = lookupBy(
+ workerInfos,
+ m => m.worker?.host,
+ );
+
+ services.forEach(s => {
+ s.workerInfo = workerInfoLookup[s.service];
+ });
} catch (e) {
+ // Swallow this error because it simply a reflection of a local
task runner.
if (
- e.response &&
- typeof e.response.data === 'object' &&
- e.response.data.error === 'Task Runner does not support worker
listing'
+ deepGet(e, 'response.data.error') !== 'Task Runner does not
support worker listing'
) {
- // Swallow this error because it simply a reflection of a local
task runner.
- middleManagers = [];
- } else {
- // Otherwise re-throw.
- throw e;
+ AppToaster.show({
+ icon: IconNames.ERROR,
+ intent: Intent.DANGER,
+ message: 'There was an error getting the worker info',
+ });
}
}
-
- const middleManagersLookup: Record<string,
MiddleManagerQueryResultRow> = lookupBy(
- middleManagers,
- m => m.worker.host,
- );
-
- services = services.map(s => {
- const middleManagerInfo = middleManagersLookup[s.service];
- if (middleManagerInfo) {
- s = { ...s, ...middleManagerInfo };
- }
- return s;
- });
}
return services;
@@ -372,7 +369,8 @@ ORDER BY
id: 'tier',
width: 180,
accessor: row => {
- return row.tier ? row.tier : row.worker ? row.worker.category :
null;
+ if (row.tier) return row.tier;
+ return deepGet(row, 'workerInfo.worker.category');
},
Cell: this.renderFilterableCell('tier'),
},
@@ -451,9 +449,11 @@ ORDER BY
className: 'padded',
accessor: row => {
if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
- return row.worker
- ? (Number(row.currCapacityUsed) || 0) /
Number(row.worker.capacity)
- : null;
+ const { workerInfo } = row;
+ if (!workerInfo) return 0;
+ return (
+ (Number(workerInfo.currCapacityUsed) || 0) /
Number(workerInfo.worker?.capacity)
+ );
} else {
return row.max_size ? Number(row.curr_size) /
Number(row.max_size) : null;
}
@@ -469,15 +469,21 @@ ORDER BY
case 'indexer':
case 'middle_manager': {
- const originalMiddleManagers: ServiceResultRow[] =
row.subRows.map(
- r => r._original,
+ const workerInfos: WorkerInfo[] = filterMap(
+ row.subRows,
+ r => r._original.workerInfo,
);
+
+ if (!workerInfos.length) {
+ return 'Could not get worker infos';
+ }
+
const totalCurrCapacityUsed = sum(
- originalMiddleManagers,
- s => Number(s.currCapacityUsed) || 0,
+ workerInfos,
+ w => Number(w.currCapacityUsed) || 0,
);
const totalWorkerCapacity = sum(
- originalMiddleManagers,
+ workerInfos,
s => deepGet(s, 'worker.capacity') || 0,
);
return `${totalCurrCapacityUsed} / ${totalWorkerCapacity}
(total slots)`;
@@ -496,8 +502,12 @@ ORDER BY
case 'indexer':
case 'middle_manager': {
- const currCapacityUsed = deepGet(row,
'original.currCapacityUsed') || 0;
- const capacity = deepGet(row, 'original.worker.capacity');
+ if (!deepGet(row, 'original.workerInfo')) {
+ return 'Could not get capacity info';
+ }
+ const currCapacityUsed =
+ deepGet(row, 'original.workerInfo.currCapacityUsed') || 0;
+ const capacity = deepGet(row,
'original.workerInfo.worker.capacity');
if (typeof capacity === 'number') {
return `Slots used: ${currCapacityUsed} of ${capacity}`;
} else {
@@ -518,30 +528,58 @@ ORDER BY
filterable: false,
className: 'padded',
accessor: row => {
- if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
- if (deepGet(row, 'worker.version') === '') return 'Disabled';
+ switch (row.service_type) {
+ case 'middle_manager':
+ case 'indexer': {
+ if (deepGet(row, 'worker.version') === '') return 'Disabled';
+ const { workerInfo } = row;
+ if (!workerInfo) {
+ return 'Could not get detail info';
+ }
- const details: string[] = [];
- if (row.lastCompletedTaskTime) {
- details.push(`Last completed task:
${row.lastCompletedTaskTime}`);
+ const details: string[] = [];
+ if (workerInfo.lastCompletedTaskTime) {
+ details.push(`Last completed task:
${workerInfo.lastCompletedTaskTime}`);
+ }
+ if (workerInfo.blacklistedUntil) {
+ details.push(`Blacklisted until:
${workerInfo.blacklistedUntil}`);
+ }
+ return details.join(' ');
}
- if (row.blacklistedUntil) {
- details.push(`Blacklisted until: ${row.blacklistedUntil}`);
+
+ case 'coordinator':
+ case 'overlord':
+ return row.is_leader === 1 ? 'Leader' : '';
+
+ case 'historical': {
+ const { loadQueueInfo } = row;
+ if (!loadQueueInfo) return 0;
+ return (
+ (Number(loadQueueInfo.segmentsToLoad) || 0) +
+ (Number(loadQueueInfo.segmentsToDrop) || 0)
+ );
}
- return details.join(' ');
- } else if (oneOf(row.service_type, 'coordinator', 'overlord')) {
- return row.is_leader === 1 ? 'Leader' : '';
- } else {
- return (Number(row.segmentsToLoad) || 0) +
(Number(row.segmentsToDrop) || 0);
+
+ default:
+ return 0;
}
},
Cell: row => {
if (row.aggregated) return '';
const { service_type } = row.original;
switch (service_type) {
+ case 'middle_manager':
+ case 'indexer':
+ case 'coordinator':
+ case 'overlord':
+ return row.value;
+
case 'historical': {
+ const { loadQueueInfo } = row.original;
+ if (!loadQueueInfo) return 'Could not get load queue info';
+
const { segmentsToLoad, segmentsToLoadSize, segmentsToDrop,
segmentsToDropSize } =
- row.original;
+ loadQueueInfo;
return formatQueues(
segmentsToLoad,
segmentsToLoadSize,
@@ -550,23 +588,31 @@ ORDER BY
);
}
- case 'indexer':
- case 'middle_manager':
- case 'coordinator':
- case 'overlord':
- return row.value;
-
default:
return '';
}
},
Aggregated: row => {
if (row.row._pivotVal !== 'historical') return '';
- const originals: ServiceResultRow[] = row.subRows.map(r =>
r._original);
- const segmentsToLoad = sum(originals, s =>
Number(s.segmentsToLoad) || 0);
- const segmentsToLoadSize = sum(originals, s =>
Number(s.segmentsToLoadSize) || 0);
- const segmentsToDrop = sum(originals, s =>
Number(s.segmentsToDrop) || 0);
- const segmentsToDropSize = sum(originals, s =>
Number(s.segmentsToDropSize) || 0);
+ const loadQueueInfos: LoadQueueInfo[] = filterMap(
+ row.subRows,
+ r => r._original.loadQueueInfo,
+ );
+
+ if (!loadQueueInfos.length) {
+ return 'Could not get load queue infos';
+ }
+
+ const segmentsToLoad = sum(loadQueueInfos, s =>
Number(s.segmentsToLoad) || 0);
+ const segmentsToLoadSize = sum(
+ loadQueueInfos,
+ s => Number(s.segmentsToLoadSize) || 0,
+ );
+ const segmentsToDrop = sum(loadQueueInfos, s =>
Number(s.segmentsToDrop) || 0);
+ const segmentsToDropSize = sum(
+ loadQueueInfos,
+ s => Number(s.segmentsToDropSize) || 0,
+ );
return formatQueues(
segmentsToLoad,
segmentsToLoadSize,
@@ -580,13 +626,14 @@ ORDER BY
show: capabilities.hasOverlordAccess() &&
visibleColumns.shown(ACTION_COLUMN_LABEL),
id: ACTION_COLUMN_ID,
width: ACTION_COLUMN_WIDTH,
- accessor: row => row.worker,
+ accessor: row => row.workerInfo,
filterable: false,
Cell: ({ value, aggregated }) => {
if (aggregated) return '';
if (!value) return null;
- const disabled = value.version === '';
- const workerActions = this.getWorkerActions(value.host,
disabled);
+ const { worker } = value;
+ const disabled = worker.version === '';
+ const workerActions = this.getWorkerActions(worker.host,
disabled);
return <ActionCell actions={workerActions} />;
},
Aggregated: () => '',
diff --git
a/web-console/src/views/workbench-view/input-source-step/example-inputs.ts
b/web-console/src/views/workbench-view/input-source-step/example-inputs.ts
index e58dfacca3..a74f1754b1 100644
--- a/web-console/src/views/workbench-view/input-source-step/example-inputs.ts
+++ b/web-console/src/views/workbench-view/input-source-step/example-inputs.ts
@@ -16,15 +16,74 @@
* limitations under the License.
*/
-import { InputSource } from '../../../druid-models';
+import { InputFormat, InputSource } from '../../../druid-models';
-export interface ExampleInputSource {
+export interface ExampleInput {
name: string;
description: string;
inputSource: InputSource;
+ inputFormat?: InputFormat;
}
-export const EXAMPLE_INPUT_SOURCES: ExampleInputSource[] = [
+const TRIPS_INPUT_FORMAT: InputFormat = {
+ type: 'csv',
+ findColumnsFromHeader: false,
+ columns: [
+ 'trip_id',
+ 'vendor_id',
+ 'pickup_datetime',
+ 'dropoff_datetime',
+ 'store_and_fwd_flag',
+ 'rate_code_id',
+ 'pickup_longitude',
+ 'pickup_latitude',
+ 'dropoff_longitude',
+ 'dropoff_latitude',
+ 'passenger_count',
+ 'trip_distance',
+ 'fare_amount',
+ 'extra',
+ 'mta_tax',
+ 'tip_amount',
+ 'tolls_amount',
+ 'ehail_fee',
+ 'improvement_surcharge',
+ 'total_amount',
+ 'payment_type',
+ 'trip_type',
+ 'pickup',
+ 'dropoff',
+ 'cab_type',
+ 'precipitation',
+ 'snow_depth',
+ 'snowfall',
+ 'max_temperature',
+ 'min_temperature',
+ 'average_wind_speed',
+ 'pickup_nyct2010_gid',
+ 'pickup_ctlabel',
+ 'pickup_borocode',
+ 'pickup_boroname',
+ 'pickup_ct2010',
+ 'pickup_boroct2010',
+ 'pickup_cdeligibil',
+ 'pickup_ntacode',
+ 'pickup_ntaname',
+ 'pickup_puma',
+ 'dropoff_nyct2010_gid',
+ 'dropoff_ctlabel',
+ 'dropoff_borocode',
+ 'dropoff_boroname',
+ 'dropoff_ct2010',
+ 'dropoff_boroct2010',
+ 'dropoff_cdeligibil',
+ 'dropoff_ntacode',
+ 'dropoff_ntaname',
+ 'dropoff_puma',
+ ],
+};
+
+export const EXAMPLE_INPUTS: ExampleInput[] = [
{
name: 'Wikipedia',
description: 'One day of wikipedia edits (JSON)',
@@ -62,6 +121,7 @@ export const EXAMPLE_INPUT_SOURCES: ExampleInputSource[] = [
'https://static.imply.io/example-data/trips/trips_xac.csv.gz',
],
},
+ inputFormat: TRIPS_INPUT_FORMAT,
},
{
name: 'NYC Taxi cabs (all files)',
@@ -145,6 +205,7 @@ export const EXAMPLE_INPUT_SOURCES: ExampleInputSource[] = [
'https://static.imply.io/example-data/trips/trips_xcv.csv.gz',
],
},
+ inputFormat: TRIPS_INPUT_FORMAT,
},
{
name: 'FlightCarrierOnTime (1 month)',
diff --git
a/web-console/src/views/workbench-view/input-source-step/input-source-step.tsx
b/web-console/src/views/workbench-view/input-source-step/input-source-step.tsx
index 211271c62c..f144e8f975 100644
---
a/web-console/src/views/workbench-view/input-source-step/input-source-step.tsx
+++
b/web-console/src/views/workbench-view/input-source-step/input-source-step.tsx
@@ -55,7 +55,7 @@ import { UrlBaser } from '../../../singletons';
import { filterMap, IntermediateQueryState } from '../../../utils';
import { postToSampler, SampleSpec } from '../../../utils/sampler';
-import { EXAMPLE_INPUT_SOURCES } from './example-inputs';
+import { EXAMPLE_INPUTS } from './example-inputs';
import { InputSourceInfo } from './input-source-info';
import './input-source-step.scss';
@@ -81,16 +81,15 @@ export const InputSourceStep = React.memo(function
InputSourceStep(props: InputS
const [inputSource, setInputSource] = useState<Partial<InputSource> | string
| undefined>(
initInputSource,
);
- const exampleInputSource = EXAMPLE_INPUT_SOURCES.find(
- ({ name }) => name === inputSource,
- )?.inputSource;
+ const exampleInput = EXAMPLE_INPUTS.find(({ name }) => name === inputSource);
const [guessedInputFormatState, connectQueryManager] = useQueryManager<
- InputSource,
+ { inputSource: InputSource; suggestedInputFormat?: InputFormat },
InputFormat,
Execution
>({
- processQuery: async (inputSource: InputSource, cancelToken) => {
+ processQuery: async ({ inputSource, suggestedInputFormat }, cancelToken)
=> {
+ let guessedInputFormat: InputFormat | undefined;
if (mode === 'sampler') {
const sampleSpec: SampleSpec = {
type: 'index_parallel',
@@ -127,7 +126,7 @@ export const InputSourceStep = React.memo(function
InputSourceStep(props: InputS
);
if (!sampleLines.length) throw new Error('No data returned from
sampler');
- return guessInputFormat(sampleLines);
+ guessedInputFormat = guessInputFormat(sampleLines);
} else {
const tableExpression = externalConfigToTableExpression({
inputSource,
@@ -151,8 +150,14 @@ export const InputSourceStep = React.memo(function
InputSourceStep(props: InputS
);
if (result instanceof IntermediateQueryState) return result;
- return resultToInputFormat(result);
+ guessedInputFormat = resultToInputFormat(result);
}
+
+ if (suggestedInputFormat?.type === guessedInputFormat.type) {
+ return suggestedInputFormat;
+ }
+
+ return guessedInputFormat;
},
backgroundStatusCheck: async (execution, query, cancelToken) => {
const result = await executionBackgroundResultStatusCheck(execution,
query, cancelToken);
@@ -164,7 +169,7 @@ export const InputSourceStep = React.memo(function
InputSourceStep(props: InputS
useEffect(() => {
const guessedInputFormat = guessedInputFormatState.data;
if (!guessedInputFormat) return;
- onSet(exampleInputSource || (inputSource as any), guessedInputFormat);
+ onSet(exampleInput?.inputSource || (inputSource as any),
guessedInputFormat);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [guessedInputFormatState]);
@@ -217,7 +222,7 @@ export const InputSourceStep = React.memo(function
InputSourceStep(props: InputS
selectedValue={inputSource}
onChange={e => setInputSource(e.currentTarget.value)}
>
- {EXAMPLE_INPUT_SOURCES.map((e, i) => (
+ {EXAMPLE_INPUTS.map((e, i) => (
<Radio
key={i}
labelElement={
@@ -306,10 +311,13 @@ export const InputSourceStep = React.memo(function
InputSourceStep(props: InputS
text={guessedInputFormatState.isLoading() ? 'Loading...' : 'Use
example'}
rightIcon={IconNames.ARROW_RIGHT}
intent={Intent.PRIMARY}
- disabled={!exampleInputSource ||
guessedInputFormatState.isLoading()}
+ disabled={!exampleInput || guessedInputFormatState.isLoading()}
onClick={() => {
- if (!exampleInputSource) return;
- connectQueryManager.runQuery(exampleInputSource);
+ if (!exampleInput) return;
+ connectQueryManager.runQuery({
+ inputSource: exampleInput.inputSource,
+ suggestedInputFormat: exampleInput.inputFormat,
+ });
}}
/>
) : inputSource ? (
@@ -324,7 +332,7 @@ export const InputSourceStep = React.memo(function
InputSourceStep(props: InputS
}
onClick={() => {
if (!AutoForm.isValidModel(inputSource, INPUT_SOURCE_FIELDS))
return;
- connectQueryManager.runQuery(inputSource);
+ connectQueryManager.runQuery({ inputSource });
}}
/>
) : undefined}
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 572120c9e7..7299760b46 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
@@ -33,6 +33,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import { MenuCheckbox, MenuTristate } from '../../../components';
import { EditContextDialog, StringInputDialog } from '../../../dialogs';
+import { IndexSpecDialog } from
'../../../dialogs/index-spec-dialog/index-spec-dialog';
import {
changeDurableShuffleStorage,
changeFinalizeAggregations,
@@ -51,9 +52,12 @@ import {
getUseApproximateCountDistinct,
getUseApproximateTopN,
getUseCache,
+ IndexSpec,
+ QueryContext,
+ summarizeIndexSpec,
WorkbenchQuery,
} from '../../../druid-models';
-import { pluralIfNeeded, tickIcon } from '../../../utils';
+import { deepGet, pluralIfNeeded, tickIcon } from '../../../utils';
import { MaxTasksButton } from '../max-tasks-button/max-tasks-button';
import './run-panel.scss';
@@ -94,6 +98,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
const { query, onQueryChange, onRun, moreMenu, loading, small, queryEngines
} = props;
const [editContextDialogOpen, setEditContextDialogOpen] = useState(false);
const [customTimezoneDialogOpen, setCustomTimezoneDialogOpen] =
useState(false);
+ const [indexSpecDialogSpec, setIndexSpecDialogSpec] = useState<IndexSpec |
undefined>();
const emptyQuery = query.isEmptyQuery();
const ingestMode = query.isIngestQuery();
@@ -104,6 +109,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
const finalizeAggregations = getFinalizeAggregations(queryContext);
const groupByEnableMultiValueUnnesting =
getGroupByEnableMultiValueUnnesting(queryContext);
const durableShuffleStorage = getDurableShuffleStorage(queryContext);
+ const indexSpec: IndexSpec | undefined = deepGet(queryContext, 'indexSpec');
const useApproximateCountDistinct =
getUseApproximateCountDistinct(queryContext);
const useApproximateTopN = getUseApproximateTopN(queryContext);
const useCache = getUseCache(queryContext);
@@ -157,6 +163,10 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
);
}
+ function changeQueryContext(queryContext: QueryContext) {
+ onQueryChange(query.changeQueryContext(queryContext));
+ }
+
const availableEngines = ([undefined] as (DruidEngine |
undefined)[]).concat(queryEngines);
function offsetOptions(): JSX.Element[] {
@@ -170,9 +180,7 @@ export const RunPanel = React.memo(function RunPanel(props:
RunPanelProps) {
icon={tickIcon(offset === timezone)}
text={offset}
shouldDismissPopover={false}
- onClick={() => {
-
onQueryChange(query.changeQueryContext(changeTimezone(queryContext, offset)));
- }}
+ onClick={() => changeQueryContext(changeTimezone(queryContext,
offset))}
/>,
);
}
@@ -233,11 +241,7 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
icon={tickIcon(!timezone)}
text="Default"
shouldDismissPopover={false}
- onClick={() => {
- onQueryChange(
-
query.changeQueryContext(changeTimezone(queryContext, undefined)),
- );
- }}
+ onClick={() =>
changeQueryContext(changeTimezone(queryContext, undefined))}
/>
<MenuItem icon={tickIcon(String(timezone).includes('/'))}
text="Named">
{NAMED_TIMEZONES.map(namedTimezone => (
@@ -246,11 +250,9 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
icon={tickIcon(namedTimezone === timezone)}
text={namedTimezone}
shouldDismissPopover={false}
- onClick={() => {
- onQueryChange(
-
query.changeQueryContext(changeTimezone(queryContext, namedTimezone)),
- );
- }}
+ onClick={() =>
+ changeQueryContext(changeTimezone(queryContext,
namedTimezone))
+ }
/>
))}
</MenuItem>
@@ -276,11 +278,9 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
key={String(v)}
icon={tickIcon(v === maxParseExceptions)}
text={v === -1 ? '∞ (-1)' : String(v)}
- onClick={() => {
- onQueryChange(
-
query.changeQueryContext(changeMaxParseExceptions(queryContext, v)),
- );
- }}
+ onClick={() =>
+
changeQueryContext(changeMaxParseExceptions(queryContext, v))
+ }
shouldDismissPopover={false}
/>
))}
@@ -290,35 +290,36 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
text="Finalize aggregations"
value={finalizeAggregations}
undefinedEffectiveValue={!ingestMode}
- onValueChange={v => {
- onQueryChange(
-
query.changeQueryContext(changeFinalizeAggregations(queryContext, v)),
- );
- }}
+ onValueChange={v =>
+
changeQueryContext(changeFinalizeAggregations(queryContext, v))
+ }
/>
<MenuTristate
icon={IconNames.FORK}
text="Enable GroupBy multi-value unnesting"
value={groupByEnableMultiValueUnnesting}
undefinedEffectiveValue={!ingestMode}
- onValueChange={v => {
- onQueryChange(
- query.changeQueryContext(
-
changeGroupByEnableMultiValueUnnesting(queryContext, v),
- ),
- );
+ onValueChange={v =>
+
changeQueryContext(changeGroupByEnableMultiValueUnnesting(queryContext, v))
+ }
+ />
+ <MenuItem
+ icon={IconNames.TH_DERIVED}
+ text="Edit index spec"
+ label={summarizeIndexSpec(indexSpec)}
+ shouldDismissPopover={false}
+ onClick={() => {
+ setIndexSpecDialogSpec(indexSpec || {});
}}
/>
<MenuCheckbox
checked={durableShuffleStorage}
text="Durable shuffle storage"
- onChange={() => {
- onQueryChange(
- query.changeQueryContext(
- changeDurableShuffleStorage(queryContext,
!durableShuffleStorage),
- ),
- );
- }}
+ onChange={() =>
+ changeQueryContext(
+ changeDurableShuffleStorage(queryContext,
!durableShuffleStorage),
+ )
+ }
/>
</>
) : (
@@ -326,22 +327,16 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
<MenuCheckbox
checked={useCache}
text="Use cache"
- onChange={() => {
- onQueryChange(
-
query.changeQueryContext(changeUseCache(queryContext, !useCache)),
- );
- }}
+ onChange={() =>
changeQueryContext(changeUseCache(queryContext, !useCache))}
/>
<MenuCheckbox
checked={useApproximateTopN}
text="Use approximate TopN"
- onChange={() => {
- onQueryChange(
- query.changeQueryContext(
- changeUseApproximateTopN(queryContext,
!useApproximateTopN),
- ),
- );
- }}
+ onChange={() =>
+ changeQueryContext(
+ changeUseApproximateTopN(queryContext,
!useApproximateTopN),
+ )
+ }
/>
</>
)}
@@ -349,16 +344,14 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
<MenuCheckbox
checked={useApproximateCountDistinct}
text="Use approximate COUNT(DISTINCT)"
- onChange={() => {
- onQueryChange(
- query.changeQueryContext(
- changeUseApproximateCountDistinct(
- queryContext,
- !useApproximateCountDistinct,
- ),
+ onChange={() =>
+ changeQueryContext(
+ changeUseApproximateCountDistinct(
+ queryContext,
+ !useApproximateCountDistinct,
),
- );
- }}
+ )
+ }
/>
)}
<MenuCheckbox
@@ -382,12 +375,7 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
/>
</Popover2>
{effectiveEngine === 'sql-msq-task' && (
- <MaxTasksButton
- queryContext={queryContext}
- changeQueryContext={queryContext =>
- onQueryChange(query.changeQueryContext(queryContext))
- }
- />
+ <MaxTasksButton queryContext={queryContext}
changeQueryContext={changeQueryContext} />
)}
</ButtonGroup>
)}
@@ -399,10 +387,7 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
{editContextDialogOpen && (
<EditContextDialog
queryContext={queryContext}
- onQueryContextChange={newContext => {
- if (!onQueryChange) return;
- onQueryChange(query.changeQueryContext(newContext));
- }}
+ onQueryContextChange={changeQueryContext}
onClose={() => {
setEditContextDialogOpen(false);
}}
@@ -413,10 +398,17 @@ export const RunPanel = React.memo(function
RunPanel(props: RunPanelProps) {
title="Custom timezone"
placeholder="Etc/UTC"
maxLength={50}
- onSubmit={tz =>
onQueryChange(query.changeQueryContext(changeTimezone(queryContext, tz)))}
+ onSubmit={tz => changeQueryContext(changeTimezone(queryContext, tz))}
onClose={() => setCustomTimezoneDialogOpen(false)}
/>
)}
+ {indexSpecDialogSpec && (
+ <IndexSpecDialog
+ onClose={() => setIndexSpecDialogSpec(undefined)}
+ onSave={indexSpec => changeQueryContext({ ...queryContext, indexSpec
})}
+ indexSpec={indexSpecDialogSpec}
+ />
+ )}
</div>
);
});
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]