diff --git a/web-console/e2e-tests/component/load-data/config/partition.ts b/web-console/e2e-tests/component/load-data/config/partition.ts index 3139484d1ba..2e168507b39 100644 --- a/web-console/e2e-tests/component/load-data/config/partition.ts +++ b/web-console/e2e-tests/component/load-data/config/partition.ts @@ -57,7 +57,11 @@ export class HashedPartitionsSpec implements PartitionsSpec { readonly type: string; static async read(page: playwright.Page): Promise { - const numShards = await getLabeledInputAsNumber(page, HashedPartitionsSpec.NUM_SHARDS); + // The shards control may not be visible in that case this is not an error, it is simply not set (null) + let numShards: number | null = null; + try { + numShards = await getLabeledInputAsNumber(page, HashedPartitionsSpec.NUM_SHARDS); + } catch {} return new HashedPartitionsSpec({ numShards }); } diff --git a/web-console/src/components/auto-form/auto-form.spec.tsx b/web-console/src/components/auto-form/auto-form.spec.tsx index c2391300a21..11dd6797723 100644 --- a/web-console/src/components/auto-form/auto-form.spec.tsx +++ b/web-console/src/components/auto-form/auto-form.spec.tsx @@ -50,7 +50,8 @@ describe('AutoForm', () => { }, { name: 'testNotDefined', type: 'string', defined: false }, - { name: 'testAdvanced', type: 'string', hideInMore: true }, + { name: 'testHide', type: 'string', hide: true }, + { name: 'testHideInMore', type: 'string', hideInMore: true }, ]} model={String} onChange={() => {}} diff --git a/web-console/src/components/auto-form/auto-form.tsx b/web-console/src/components/auto-form/auto-form.tsx index 5c8e0f63156..ee67720486a 100644 --- a/web-console/src/components/auto-form/auto-form.tsx +++ b/web-console/src/components/auto-form/auto-form.tsx @@ -56,6 +56,7 @@ export interface Field { disabled?: Functor; defined?: Functor; required?: Functor; + hide?: Functor; hideInMore?: Functor; valueAdjustment?: (value: any) => any; adjustment?: (model: M) => M; @@ -456,10 +457,15 @@ export class AutoForm> extends React.PureComponent let shouldShowMore = false; const shownFields = fields.filter(field => { if (AutoForm.evaluateFunctor(field.defined, model, true)) { + if (AutoForm.evaluateFunctor(field.hide, model, false)) { + return false; + } + if (AutoForm.evaluateFunctor(field.hideInMore, model, false)) { shouldShowMore = true; return showMore; } + return true; } else { return false; diff --git a/web-console/src/components/json-input/json-input.tsx b/web-console/src/components/json-input/json-input.tsx index dcf9a269da5..9ebd5e5f4b4 100644 --- a/web-console/src/components/json-input/json-input.tsx +++ b/web-console/src/components/json-input/json-input.tsx @@ -67,6 +67,7 @@ interface InternalValue { interface JsonInputProps { value: any; onChange: (value: any) => void; + onError?: (error: Error) => void; placeholder?: string; focus?: boolean; width?: string; @@ -75,7 +76,7 @@ interface JsonInputProps { } export const JsonInput = React.memo(function JsonInput(props: JsonInputProps) { - const { onChange, placeholder, focus, width, height, value, issueWithValue } = props; + const { onChange, onError, placeholder, focus, width, height, value, issueWithValue } = props; const [internalValue, setInternalValue] = useState(() => ({ value, stringified: stringifyJson(value), @@ -120,7 +121,9 @@ export const JsonInput = React.memo(function JsonInput(props: JsonInputProps) { stringified: inputJson, }); - if (!error) { + if (error) { + onError?.(error); + } else { onChange(value); } diff --git a/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap b/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap index ba969a43086..c7e89303c9d 100644 --- a/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap +++ b/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap @@ -86,6 +86,28 @@ exports[`CompactionDialog matches snapshot with compactionConfig (dynamic partit
+ Target number of rows to include in a partition, should be a number that targets segments of 500MB~1GB. +
+ + maxRowsPerSegment + + is an alias for + + targetRowsPerSegment + + . Only one of these properties can be used. +
diff --git a/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx index f4260e619d2..c0e02af84cf 100644 --- a/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx +++ b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx @@ -46,6 +46,7 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn const { onClose } = props; const [currentTab, setCurrentTab] = useState('form'); const [dynamicConfig, setDynamicConfig] = useState({}); + const [jsonError, setJsonError] = useState(); const [historyRecordsState] = useQueryManager({ processQuery: async () => { @@ -100,6 +101,7 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn return ( ) : ( - + { + setDynamicConfig(v); + setJsonError(undefined); + }} + onError={setJsonError} + /> )} ); diff --git a/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx b/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx index ba506ae9d97..889f5879f27 100644 --- a/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx +++ b/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx @@ -58,6 +58,11 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look } = props; const [currentTab, setCurrentTab] = useState('form'); const [updateVersionOnSubmit, setUpdateVersionOnSubmit] = useState(true); + const [jsonError, setJsonError] = useState(); + + const disableSubmit = Boolean( + jsonError || isLookupInvalid(lookupName, lookupVersion, lookupTier, lookupSpec), + ); return ( { onChange('spec', m); + setJsonError(undefined); }} - height="80vh" + onError={setJsonError} /> )} @@ -135,10 +142,10 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look { onSubmit(updateVersionOnSubmit && isEdit); }} - disabled={isLookupInvalid(lookupName, lookupVersion, lookupTier, lookupSpec)} /> diff --git a/web-console/src/druid-models/compaction-config.tsx b/web-console/src/druid-models/compaction-config.tsx index fb87d2825f9..f7286e435fe 100644 --- a/web-console/src/druid-models/compaction-config.tsx +++ b/web-console/src/druid-models/compaction-config.tsx @@ -70,9 +70,11 @@ export const COMPACTION_CONFIG_FIELDS: Field[] = [ name: 'tuningConfig.partitionsSpec.targetRowsPerSegment', type: 'number', zeroMeansUndefined: true, + placeholder: `(defaults to 500000)`, defined: (t: CompactionConfig) => deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' && - !deepGet(t, 'tuningConfig.partitionsSpec.numShards'), + !deepGet(t, 'tuningConfig.partitionsSpec.numShards') && + !deepGet(t, 'tuningConfig.partitionsSpec.maxRowsPerSegment'), info: ( <> @@ -86,12 +88,34 @@ export const COMPACTION_CONFIG_FIELDS: Field[] = [ > ), }, + { + name: 'tuningConfig.partitionsSpec.maxRowsPerSegment', + type: 'number', + zeroMeansUndefined: true, + defined: (t: CompactionConfig) => + deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' && + !deepGet(t, 'tuningConfig.partitionsSpec.numShards') && + !deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'), + info: ( + <> + + Target number of rows to include in a partition, should be a number that targets segments + of 500MB~1GB. + + + maxRowsPerSegment is an alias for targetRowsPerSegment. Only one + of these properties can be used. + + > + ), + }, { name: 'tuningConfig.partitionsSpec.numShards', type: 'number', zeroMeansUndefined: true, defined: (t: CompactionConfig) => deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' && + !deepGet(t, 'tuningConfig.partitionsSpec.maxRowsPerSegment') && !deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'), info: ( <> diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 4b85ba29012..3557debad6f 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -22,6 +22,7 @@ import copy from 'copy-to-clipboard'; import { SqlExpression, SqlFunction, SqlLiteral, SqlRef } from 'druid-query-toolkit'; import FileSaver from 'file-saver'; import hasOwnProp from 'has-own-prop'; +import * as JSONBig from 'json-bigint-native'; import numeral from 'numeral'; import React from 'react'; import { Filter, FilterRender } from 'react-table'; @@ -382,3 +383,15 @@ export function moveElement(items: readonly T[], fromIndex: number, toIndex: return items.slice(); } } + +export function stringifyValue(value: unknown): string { + switch (typeof value) { + case 'object': + if (!value) return String(value); + if (typeof (value as any).toISOString === 'function') return (value as any).toISOString(); + return JSONBig.stringify(value); + + default: + return String(value); + } +} diff --git a/web-console/src/views/query-view/query-output/query-output.tsx b/web-console/src/views/query-view/query-output/query-output.tsx index bd3622da7ed..2dbeef44714 100644 --- a/web-console/src/views/query-view/query-output/query-output.tsx +++ b/web-console/src/views/query-view/query-output/query-output.tsx @@ -27,13 +27,12 @@ import { SqlRef, trimString, } from 'druid-query-toolkit'; -import * as JSONBig from 'json-bigint-native'; import React, { useEffect, useState } from 'react'; import ReactTable from 'react-table'; import { BracedText, TableCell } from '../../../components'; import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog'; -import { copyAndAlert, deepSet, filterMap, prettyPrintSql } from '../../../utils'; +import { copyAndAlert, deepSet, filterMap, prettyPrintSql, stringifyValue } from '../../../utils'; import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action'; import { ColumnRenameInput } from './column-rename-input/column-rename-input'; @@ -44,18 +43,6 @@ function isComparable(x: unknown): boolean { return x !== null && x !== '' && !isNaN(Number(x)); } -function stringifyValue(value: unknown): string { - switch (typeof value) { - case 'object': - if (!value) return String(value); - if (typeof (value as any).toISOString === 'function') return (value as any).toISOString(); - return JSONBig.stringify(value); - - default: - return String(value); - } -} - interface Pagination { page: number; pageSize: number; diff --git a/web-console/src/views/query-view/query-view.spec.tsx b/web-console/src/views/query-view/query-view.spec.tsx index 9e806de33ec..4e331320d35 100644 --- a/web-console/src/views/query-view/query-view.spec.tsx +++ b/web-console/src/views/query-view/query-view.spec.tsx @@ -32,11 +32,20 @@ describe('QueryView', () => { expect(sqlView).toMatchSnapshot(); }); - it('trimSemicolon', () => { + it('.trimSemicolon', () => { expect(QueryView.trimSemicolon('SELECT * FROM tbl;')).toEqual('SELECT * FROM tbl'); expect(QueryView.trimSemicolon('SELECT * FROM tbl; ')).toEqual('SELECT * FROM tbl '); expect(QueryView.trimSemicolon('SELECT * FROM tbl; --hello ')).toEqual( 'SELECT * FROM tbl --hello ', ); }); + + it('.formatStr', () => { + expect(QueryView.formatStr(null, 'csv')).toEqual('"null"'); + expect(QueryView.formatStr('hello\nworld', 'csv')).toEqual('"hello world"'); + expect(QueryView.formatStr(123, 'csv')).toEqual('"123"'); + expect(QueryView.formatStr(new Date('2021-01-02T03:04:05.678Z'), 'csv')).toEqual( + '"2021-01-02T03:04:05.678Z"', + ); + }); }); diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx index 53dc1d9cf69..3612a269a41 100644 --- a/web-console/src/views/query-view/query-view.tsx +++ b/web-console/src/views/query-view/query-view.tsx @@ -48,6 +48,7 @@ import { QueryState, RowColumn, SemiJoinQueryExplanation, + stringifyValue, } from '../../utils'; import { isEmptyContext, QueryContext } from '../../utils/query-context'; import { QueryRecord, QueryRecordUtil } from '../../utils/query-history'; @@ -142,19 +143,16 @@ export class QueryView extends React.PureComponent double quote, handle ',' - return `"${String(s) - .replace(/(?:\r\n|\r|\n)/g, ' ') - .replace(/"/g, '""')}"`; + // csv: single quote => double quote, handle ',' + return `"${str.replace(/"/g, '""')}"`; } else { - // tsv - // remove line break, single quote => double quote, \t => '' - return String(s) - .replace(/(?:\r\n|\r|\n)/g, ' ') - .replace(/\t/g, '') - .replace(/"/g, '""'); + // tsv: single quote => double quote, \t => '' + return str.replace(/\t/g, '').replace(/"/g, '""'); } }
@@ -86,12 +88,34 @@ export const COMPACTION_CONFIG_FIELDS: Field[] = [ > ), }, + { + name: 'tuningConfig.partitionsSpec.maxRowsPerSegment', + type: 'number', + zeroMeansUndefined: true, + defined: (t: CompactionConfig) => + deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' && + !deepGet(t, 'tuningConfig.partitionsSpec.numShards') && + !deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'), + info: ( + <> + + Target number of rows to include in a partition, should be a number that targets segments + of 500MB~1GB. + + + maxRowsPerSegment is an alias for targetRowsPerSegment. Only one + of these properties can be used. + + > + ), + }, { name: 'tuningConfig.partitionsSpec.numShards', type: 'number', zeroMeansUndefined: true, defined: (t: CompactionConfig) => deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' && + !deepGet(t, 'tuningConfig.partitionsSpec.maxRowsPerSegment') && !deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'), info: ( <> diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 4b85ba29012..3557debad6f 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -22,6 +22,7 @@ import copy from 'copy-to-clipboard'; import { SqlExpression, SqlFunction, SqlLiteral, SqlRef } from 'druid-query-toolkit'; import FileSaver from 'file-saver'; import hasOwnProp from 'has-own-prop'; +import * as JSONBig from 'json-bigint-native'; import numeral from 'numeral'; import React from 'react'; import { Filter, FilterRender } from 'react-table'; @@ -382,3 +383,15 @@ export function moveElement(items: readonly T[], fromIndex: number, toIndex: return items.slice(); } } + +export function stringifyValue(value: unknown): string { + switch (typeof value) { + case 'object': + if (!value) return String(value); + if (typeof (value as any).toISOString === 'function') return (value as any).toISOString(); + return JSONBig.stringify(value); + + default: + return String(value); + } +} diff --git a/web-console/src/views/query-view/query-output/query-output.tsx b/web-console/src/views/query-view/query-output/query-output.tsx index bd3622da7ed..2dbeef44714 100644 --- a/web-console/src/views/query-view/query-output/query-output.tsx +++ b/web-console/src/views/query-view/query-output/query-output.tsx @@ -27,13 +27,12 @@ import { SqlRef, trimString, } from 'druid-query-toolkit'; -import * as JSONBig from 'json-bigint-native'; import React, { useEffect, useState } from 'react'; import ReactTable from 'react-table'; import { BracedText, TableCell } from '../../../components'; import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog'; -import { copyAndAlert, deepSet, filterMap, prettyPrintSql } from '../../../utils'; +import { copyAndAlert, deepSet, filterMap, prettyPrintSql, stringifyValue } from '../../../utils'; import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action'; import { ColumnRenameInput } from './column-rename-input/column-rename-input'; @@ -44,18 +43,6 @@ function isComparable(x: unknown): boolean { return x !== null && x !== '' && !isNaN(Number(x)); } -function stringifyValue(value: unknown): string { - switch (typeof value) { - case 'object': - if (!value) return String(value); - if (typeof (value as any).toISOString === 'function') return (value as any).toISOString(); - return JSONBig.stringify(value); - - default: - return String(value); - } -} - interface Pagination { page: number; pageSize: number; diff --git a/web-console/src/views/query-view/query-view.spec.tsx b/web-console/src/views/query-view/query-view.spec.tsx index 9e806de33ec..4e331320d35 100644 --- a/web-console/src/views/query-view/query-view.spec.tsx +++ b/web-console/src/views/query-view/query-view.spec.tsx @@ -32,11 +32,20 @@ describe('QueryView', () => { expect(sqlView).toMatchSnapshot(); }); - it('trimSemicolon', () => { + it('.trimSemicolon', () => { expect(QueryView.trimSemicolon('SELECT * FROM tbl;')).toEqual('SELECT * FROM tbl'); expect(QueryView.trimSemicolon('SELECT * FROM tbl; ')).toEqual('SELECT * FROM tbl '); expect(QueryView.trimSemicolon('SELECT * FROM tbl; --hello ')).toEqual( 'SELECT * FROM tbl --hello ', ); }); + + it('.formatStr', () => { + expect(QueryView.formatStr(null, 'csv')).toEqual('"null"'); + expect(QueryView.formatStr('hello\nworld', 'csv')).toEqual('"hello world"'); + expect(QueryView.formatStr(123, 'csv')).toEqual('"123"'); + expect(QueryView.formatStr(new Date('2021-01-02T03:04:05.678Z'), 'csv')).toEqual( + '"2021-01-02T03:04:05.678Z"', + ); + }); }); diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx index 53dc1d9cf69..3612a269a41 100644 --- a/web-console/src/views/query-view/query-view.tsx +++ b/web-console/src/views/query-view/query-view.tsx @@ -48,6 +48,7 @@ import { QueryState, RowColumn, SemiJoinQueryExplanation, + stringifyValue, } from '../../utils'; import { isEmptyContext, QueryContext } from '../../utils/query-context'; import { QueryRecord, QueryRecordUtil } from '../../utils/query-history'; @@ -142,19 +143,16 @@ export class QueryView extends React.PureComponent double quote, handle ',' - return `"${String(s) - .replace(/(?:\r\n|\r|\n)/g, ' ') - .replace(/"/g, '""')}"`; + // csv: single quote => double quote, handle ',' + return `"${str.replace(/"/g, '""')}"`; } else { - // tsv - // remove line break, single quote => double quote, \t => '' - return String(s) - .replace(/(?:\r\n|\r|\n)/g, ' ') - .replace(/\t/g, '') - .replace(/"/g, '""'); + // tsv: single quote => double quote, \t => '' + return str.replace(/\t/g, '').replace(/"/g, '""'); } }
+ Target number of rows to include in a partition, should be a number that targets segments + of 500MB~1GB. +
+ maxRowsPerSegment is an alias for targetRowsPerSegment. Only one + of these properties can be used. +
maxRowsPerSegment
targetRowsPerSegment