mirror of https://github.com/apache/druid.git
Web console: Fix maxRowsPerSegment validation in hashed compaction spec (#11308)
* allow defining of maxRowsPerSegment for now * use common util * update snapshots * fix test * fix e2e test
This commit is contained in:
parent
e5633d7842
commit
31c811d894
|
@ -57,7 +57,11 @@ export class HashedPartitionsSpec implements PartitionsSpec {
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
|
|
||||||
static async read(page: playwright.Page): Promise<HashedPartitionsSpec> {
|
static async read(page: playwright.Page): Promise<HashedPartitionsSpec> {
|
||||||
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 });
|
return new HashedPartitionsSpec({ numShards });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,8 @@ describe('AutoForm', () => {
|
||||||
},
|
},
|
||||||
|
|
||||||
{ name: 'testNotDefined', type: 'string', defined: false },
|
{ 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}
|
model={String}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
|
|
|
@ -56,6 +56,7 @@ export interface Field<M> {
|
||||||
disabled?: Functor<M, boolean>;
|
disabled?: Functor<M, boolean>;
|
||||||
defined?: Functor<M, boolean>;
|
defined?: Functor<M, boolean>;
|
||||||
required?: Functor<M, boolean>;
|
required?: Functor<M, boolean>;
|
||||||
|
hide?: Functor<M, boolean>;
|
||||||
hideInMore?: Functor<M, boolean>;
|
hideInMore?: Functor<M, boolean>;
|
||||||
valueAdjustment?: (value: any) => any;
|
valueAdjustment?: (value: any) => any;
|
||||||
adjustment?: (model: M) => M;
|
adjustment?: (model: M) => M;
|
||||||
|
@ -456,10 +457,15 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
||||||
let shouldShowMore = false;
|
let shouldShowMore = false;
|
||||||
const shownFields = fields.filter(field => {
|
const shownFields = fields.filter(field => {
|
||||||
if (AutoForm.evaluateFunctor(field.defined, model, true)) {
|
if (AutoForm.evaluateFunctor(field.defined, model, true)) {
|
||||||
|
if (AutoForm.evaluateFunctor(field.hide, model, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (AutoForm.evaluateFunctor(field.hideInMore, model, false)) {
|
if (AutoForm.evaluateFunctor(field.hideInMore, model, false)) {
|
||||||
shouldShowMore = true;
|
shouldShowMore = true;
|
||||||
return showMore;
|
return showMore;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -67,6 +67,7 @@ interface InternalValue {
|
||||||
interface JsonInputProps {
|
interface JsonInputProps {
|
||||||
value: any;
|
value: any;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
width?: string;
|
width?: string;
|
||||||
|
@ -75,7 +76,7 @@ interface JsonInputProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JsonInput = React.memo(function JsonInput(props: 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<InternalValue>(() => ({
|
const [internalValue, setInternalValue] = useState<InternalValue>(() => ({
|
||||||
value,
|
value,
|
||||||
stringified: stringifyJson(value),
|
stringified: stringifyJson(value),
|
||||||
|
@ -120,7 +121,9 @@ export const JsonInput = React.memo(function JsonInput(props: JsonInputProps) {
|
||||||
stringified: inputJson,
|
stringified: inputJson,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!error) {
|
if (error) {
|
||||||
|
onError?.(error);
|
||||||
|
} else {
|
||||||
onChange(value);
|
onChange(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,28 @@ exports[`CompactionDialog matches snapshot with compactionConfig (dynamic partit
|
||||||
</p>
|
</p>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
"name": "tuningConfig.partitionsSpec.targetRowsPerSegment",
|
"name": "tuningConfig.partitionsSpec.targetRowsPerSegment",
|
||||||
|
"placeholder": "(defaults to 500000)",
|
||||||
|
"type": "number",
|
||||||
|
"zeroMeansUndefined": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"defined": [Function],
|
||||||
|
"info": <React.Fragment>
|
||||||
|
<p>
|
||||||
|
Target number of rows to include in a partition, should be a number that targets segments of 500MB~1GB.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Unknown>
|
||||||
|
maxRowsPerSegment
|
||||||
|
</Unknown>
|
||||||
|
is an alias for
|
||||||
|
<Unknown>
|
||||||
|
targetRowsPerSegment
|
||||||
|
</Unknown>
|
||||||
|
. Only one of these properties can be used.
|
||||||
|
</p>
|
||||||
|
</React.Fragment>,
|
||||||
|
"name": "tuningConfig.partitionsSpec.maxRowsPerSegment",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"zeroMeansUndefined": true,
|
"zeroMeansUndefined": true,
|
||||||
},
|
},
|
||||||
|
@ -335,6 +357,28 @@ exports[`CompactionDialog matches snapshot with compactionConfig (hashed partiti
|
||||||
</p>
|
</p>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
"name": "tuningConfig.partitionsSpec.targetRowsPerSegment",
|
"name": "tuningConfig.partitionsSpec.targetRowsPerSegment",
|
||||||
|
"placeholder": "(defaults to 500000)",
|
||||||
|
"type": "number",
|
||||||
|
"zeroMeansUndefined": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"defined": [Function],
|
||||||
|
"info": <React.Fragment>
|
||||||
|
<p>
|
||||||
|
Target number of rows to include in a partition, should be a number that targets segments of 500MB~1GB.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Unknown>
|
||||||
|
maxRowsPerSegment
|
||||||
|
</Unknown>
|
||||||
|
is an alias for
|
||||||
|
<Unknown>
|
||||||
|
targetRowsPerSegment
|
||||||
|
</Unknown>
|
||||||
|
. Only one of these properties can be used.
|
||||||
|
</p>
|
||||||
|
</React.Fragment>,
|
||||||
|
"name": "tuningConfig.partitionsSpec.maxRowsPerSegment",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"zeroMeansUndefined": true,
|
"zeroMeansUndefined": true,
|
||||||
},
|
},
|
||||||
|
@ -584,6 +628,28 @@ exports[`CompactionDialog matches snapshot with compactionConfig (single_dim par
|
||||||
</p>
|
</p>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
"name": "tuningConfig.partitionsSpec.targetRowsPerSegment",
|
"name": "tuningConfig.partitionsSpec.targetRowsPerSegment",
|
||||||
|
"placeholder": "(defaults to 500000)",
|
||||||
|
"type": "number",
|
||||||
|
"zeroMeansUndefined": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"defined": [Function],
|
||||||
|
"info": <React.Fragment>
|
||||||
|
<p>
|
||||||
|
Target number of rows to include in a partition, should be a number that targets segments of 500MB~1GB.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Unknown>
|
||||||
|
maxRowsPerSegment
|
||||||
|
</Unknown>
|
||||||
|
is an alias for
|
||||||
|
<Unknown>
|
||||||
|
targetRowsPerSegment
|
||||||
|
</Unknown>
|
||||||
|
. Only one of these properties can be used.
|
||||||
|
</p>
|
||||||
|
</React.Fragment>,
|
||||||
|
"name": "tuningConfig.partitionsSpec.maxRowsPerSegment",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"zeroMeansUndefined": true,
|
"zeroMeansUndefined": true,
|
||||||
},
|
},
|
||||||
|
@ -833,6 +899,28 @@ exports[`CompactionDialog matches snapshot without compactionConfig 1`] = `
|
||||||
</p>
|
</p>
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
"name": "tuningConfig.partitionsSpec.targetRowsPerSegment",
|
"name": "tuningConfig.partitionsSpec.targetRowsPerSegment",
|
||||||
|
"placeholder": "(defaults to 500000)",
|
||||||
|
"type": "number",
|
||||||
|
"zeroMeansUndefined": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"defined": [Function],
|
||||||
|
"info": <React.Fragment>
|
||||||
|
<p>
|
||||||
|
Target number of rows to include in a partition, should be a number that targets segments of 500MB~1GB.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Unknown>
|
||||||
|
maxRowsPerSegment
|
||||||
|
</Unknown>
|
||||||
|
is an alias for
|
||||||
|
<Unknown>
|
||||||
|
targetRowsPerSegment
|
||||||
|
</Unknown>
|
||||||
|
. Only one of these properties can be used.
|
||||||
|
</p>
|
||||||
|
</React.Fragment>,
|
||||||
|
"name": "tuningConfig.partitionsSpec.maxRowsPerSegment",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"zeroMeansUndefined": true,
|
"zeroMeansUndefined": true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -42,12 +42,10 @@ export const CompactionDialog = React.memo(function CompactionDialog(props: Comp
|
||||||
tuningConfig: { partitionsSpec: { type: 'dynamic' } },
|
tuningConfig: { partitionsSpec: { type: 'dynamic' } },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const [jsonError, setJsonError] = useState<Error | undefined>();
|
||||||
|
|
||||||
const issueWithCurrentConfig = AutoForm.issueWithModel(currentConfig, COMPACTION_CONFIG_FIELDS);
|
const issueWithCurrentConfig = AutoForm.issueWithModel(currentConfig, COMPACTION_CONFIG_FIELDS);
|
||||||
function handleSubmit() {
|
const disableSubmit = Boolean(jsonError || issueWithCurrentConfig);
|
||||||
if (issueWithCurrentConfig) return;
|
|
||||||
onSave(currentConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -68,7 +66,11 @@ export const CompactionDialog = React.memo(function CompactionDialog(props: Comp
|
||||||
) : (
|
) : (
|
||||||
<JsonInput
|
<JsonInput
|
||||||
value={currentConfig}
|
value={currentConfig}
|
||||||
onChange={setCurrentConfig}
|
onChange={v => {
|
||||||
|
setCurrentConfig(v);
|
||||||
|
setJsonError(undefined);
|
||||||
|
}}
|
||||||
|
onError={setJsonError}
|
||||||
issueWithValue={value => AutoForm.issueWithModel(value, COMPACTION_CONFIG_FIELDS)}
|
issueWithValue={value => AutoForm.issueWithModel(value, COMPACTION_CONFIG_FIELDS)}
|
||||||
height="100%"
|
height="100%"
|
||||||
/>
|
/>
|
||||||
|
@ -81,8 +83,8 @@ export const CompactionDialog = React.memo(function CompactionDialog(props: Comp
|
||||||
<Button
|
<Button
|
||||||
text="Submit"
|
text="Submit"
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
onClick={handleSubmit}
|
disabled={disableSubmit}
|
||||||
disabled={Boolean(issueWithCurrentConfig)}
|
onClick={() => onSave(currentConfig)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
|
||||||
className="coordinator-dynamic-config-dialog"
|
className="coordinator-dynamic-config-dialog"
|
||||||
onClose={[Function]}
|
onClose={[Function]}
|
||||||
onSave={[Function]}
|
onSave={[Function]}
|
||||||
|
saveDisabled={false}
|
||||||
title="Coordinator dynamic config"
|
title="Coordinator dynamic config"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -46,6 +46,7 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn
|
||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
const [currentTab, setCurrentTab] = useState<FormJsonTabs>('form');
|
const [currentTab, setCurrentTab] = useState<FormJsonTabs>('form');
|
||||||
const [dynamicConfig, setDynamicConfig] = useState<CoordinatorDynamicConfig>({});
|
const [dynamicConfig, setDynamicConfig] = useState<CoordinatorDynamicConfig>({});
|
||||||
|
const [jsonError, setJsonError] = useState<Error | undefined>();
|
||||||
|
|
||||||
const [historyRecordsState] = useQueryManager<null, any[]>({
|
const [historyRecordsState] = useQueryManager<null, any[]>({
|
||||||
processQuery: async () => {
|
processQuery: async () => {
|
||||||
|
@ -100,6 +101,7 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn
|
||||||
return (
|
return (
|
||||||
<SnitchDialog
|
<SnitchDialog
|
||||||
className="coordinator-dynamic-config-dialog"
|
className="coordinator-dynamic-config-dialog"
|
||||||
|
saveDisabled={Boolean(jsonError)}
|
||||||
onSave={saveConfig}
|
onSave={saveConfig}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title="Coordinator dynamic config"
|
title="Coordinator dynamic config"
|
||||||
|
@ -121,7 +123,15 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn
|
||||||
onChange={setDynamicConfig}
|
onChange={setDynamicConfig}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<JsonInput value={dynamicConfig} onChange={setDynamicConfig} height="50vh" />
|
<JsonInput
|
||||||
|
value={dynamicConfig}
|
||||||
|
height="50vh"
|
||||||
|
onChange={v => {
|
||||||
|
setDynamicConfig(v);
|
||||||
|
setJsonError(undefined);
|
||||||
|
}}
|
||||||
|
onError={setJsonError}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</SnitchDialog>
|
</SnitchDialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -58,6 +58,11 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
|
||||||
} = props;
|
} = props;
|
||||||
const [currentTab, setCurrentTab] = useState<FormJsonTabs>('form');
|
const [currentTab, setCurrentTab] = useState<FormJsonTabs>('form');
|
||||||
const [updateVersionOnSubmit, setUpdateVersionOnSubmit] = useState(true);
|
const [updateVersionOnSubmit, setUpdateVersionOnSubmit] = useState(true);
|
||||||
|
const [jsonError, setJsonError] = useState<Error | undefined>();
|
||||||
|
|
||||||
|
const disableSubmit = Boolean(
|
||||||
|
jsonError || isLookupInvalid(lookupName, lookupVersion, lookupTier, lookupSpec),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -122,10 +127,12 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
|
||||||
) : (
|
) : (
|
||||||
<JsonInput
|
<JsonInput
|
||||||
value={lookupSpec}
|
value={lookupSpec}
|
||||||
|
height="80vh"
|
||||||
onChange={m => {
|
onChange={m => {
|
||||||
onChange('spec', m);
|
onChange('spec', m);
|
||||||
|
setJsonError(undefined);
|
||||||
}}
|
}}
|
||||||
height="80vh"
|
onError={setJsonError}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,10 +142,10 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
|
||||||
<Button
|
<Button
|
||||||
text="Submit"
|
text="Submit"
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
|
disabled={disableSubmit}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSubmit(updateVersionOnSubmit && isEdit);
|
onSubmit(updateVersionOnSubmit && isEdit);
|
||||||
}}
|
}}
|
||||||
disabled={isLookupInvalid(lookupName, lookupVersion, lookupTier, lookupSpec)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,9 +70,11 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
|
||||||
name: 'tuningConfig.partitionsSpec.targetRowsPerSegment',
|
name: 'tuningConfig.partitionsSpec.targetRowsPerSegment',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
zeroMeansUndefined: true,
|
zeroMeansUndefined: true,
|
||||||
|
placeholder: `(defaults to 500000)`,
|
||||||
defined: (t: CompactionConfig) =>
|
defined: (t: CompactionConfig) =>
|
||||||
deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' &&
|
deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' &&
|
||||||
!deepGet(t, 'tuningConfig.partitionsSpec.numShards'),
|
!deepGet(t, 'tuningConfig.partitionsSpec.numShards') &&
|
||||||
|
!deepGet(t, 'tuningConfig.partitionsSpec.maxRowsPerSegment'),
|
||||||
info: (
|
info: (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>
|
||||||
|
@ -86,12 +88,34 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Target number of rows to include in a partition, should be a number that targets segments
|
||||||
|
of 500MB~1GB.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Code>maxRowsPerSegment</Code> is an alias for <Code>targetRowsPerSegment</Code>. Only one
|
||||||
|
of these properties can be used.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'tuningConfig.partitionsSpec.numShards',
|
name: 'tuningConfig.partitionsSpec.numShards',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
zeroMeansUndefined: true,
|
zeroMeansUndefined: true,
|
||||||
defined: (t: CompactionConfig) =>
|
defined: (t: CompactionConfig) =>
|
||||||
deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' &&
|
deepGet(t, 'tuningConfig.partitionsSpec.type') === 'hashed' &&
|
||||||
|
!deepGet(t, 'tuningConfig.partitionsSpec.maxRowsPerSegment') &&
|
||||||
!deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'),
|
!deepGet(t, 'tuningConfig.partitionsSpec.targetRowsPerSegment'),
|
||||||
info: (
|
info: (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import copy from 'copy-to-clipboard';
|
||||||
import { SqlExpression, SqlFunction, SqlLiteral, SqlRef } from 'druid-query-toolkit';
|
import { SqlExpression, SqlFunction, SqlLiteral, SqlRef } from 'druid-query-toolkit';
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import hasOwnProp from 'has-own-prop';
|
import hasOwnProp from 'has-own-prop';
|
||||||
|
import * as JSONBig from 'json-bigint-native';
|
||||||
import numeral from 'numeral';
|
import numeral from 'numeral';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Filter, FilterRender } from 'react-table';
|
import { Filter, FilterRender } from 'react-table';
|
||||||
|
@ -382,3 +383,15 @@ export function moveElement<T>(items: readonly T[], fromIndex: number, toIndex:
|
||||||
return items.slice();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,13 +27,12 @@ import {
|
||||||
SqlRef,
|
SqlRef,
|
||||||
trimString,
|
trimString,
|
||||||
} from 'druid-query-toolkit';
|
} from 'druid-query-toolkit';
|
||||||
import * as JSONBig from 'json-bigint-native';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
import { BracedText, TableCell } from '../../../components';
|
import { BracedText, TableCell } from '../../../components';
|
||||||
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
|
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 { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
|
||||||
|
|
||||||
import { ColumnRenameInput } from './column-rename-input/column-rename-input';
|
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));
|
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 {
|
interface Pagination {
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
|
|
@ -32,11 +32,20 @@ describe('QueryView', () => {
|
||||||
expect(sqlView).toMatchSnapshot();
|
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; ')).toEqual('SELECT * FROM tbl ');
|
expect(QueryView.trimSemicolon('SELECT * FROM tbl; ')).toEqual('SELECT * FROM tbl ');
|
||||||
expect(QueryView.trimSemicolon('SELECT * FROM tbl; --hello ')).toEqual(
|
expect(QueryView.trimSemicolon('SELECT * FROM tbl; --hello ')).toEqual(
|
||||||
'SELECT * FROM tbl --hello ',
|
'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"',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {
|
||||||
QueryState,
|
QueryState,
|
||||||
RowColumn,
|
RowColumn,
|
||||||
SemiJoinQueryExplanation,
|
SemiJoinQueryExplanation,
|
||||||
|
stringifyValue,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { isEmptyContext, QueryContext } from '../../utils/query-context';
|
import { isEmptyContext, QueryContext } from '../../utils/query-context';
|
||||||
import { QueryRecord, QueryRecordUtil } from '../../utils/query-history';
|
import { QueryRecord, QueryRecordUtil } from '../../utils/query-history';
|
||||||
|
@ -142,19 +143,16 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static formatStr(s: string | number, format: 'csv' | 'tsv') {
|
static formatStr(s: null | string | number | Date, format: 'csv' | 'tsv') {
|
||||||
|
// stringify and remove line break
|
||||||
|
const str = stringifyValue(s).replace(/(?:\r\n|\r|\n)/g, ' ');
|
||||||
|
|
||||||
if (format === 'csv') {
|
if (format === 'csv') {
|
||||||
// remove line break, single quote => double quote, handle ','
|
// csv: single quote => double quote, handle ','
|
||||||
return `"${String(s)
|
return `"${str.replace(/"/g, '""')}"`;
|
||||||
.replace(/(?:\r\n|\r|\n)/g, ' ')
|
|
||||||
.replace(/"/g, '""')}"`;
|
|
||||||
} else {
|
} else {
|
||||||
// tsv
|
// tsv: single quote => double quote, \t => ''
|
||||||
// remove line break, single quote => double quote, \t => ''
|
return str.replace(/\t/g, '').replace(/"/g, '""');
|
||||||
return String(s)
|
|
||||||
.replace(/(?:\r\n|\r|\n)/g, ' ')
|
|
||||||
.replace(/\t/g, '')
|
|
||||||
.replace(/"/g, '""');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue