You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by vo...@apache.org on 2021/07/10 14:57:19 UTC
[druid] branch master updated: Web console: Data loading
walkthrough fixes (#11416)
This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 377b5e7 Web console: Data loading walkthrough fixes (#11416)
377b5e7 is described below
commit 377b5e708c926fea3b53e6f22cc1e12262fb62e0
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Sat Jul 10 07:56:50 2021 -0700
Web console: Data loading walkthrough fixes (#11416)
* fix quotes
* fix sql doc parsing
* prevent array-input from losing position while the user is typing
* make group filter click-to-filterable
* fix casing bug in exact table search
* do not sort columns in smaples
* can bypass transform step
* fixed string json parsing
* improve PartitionMessage
* better error messages
* feedback fixes
* tool to order dimensions in schema view
---
licenses.yaml | 2 +-
web-console/package-lock.json | 6 +-
web-console/package.json | 2 +-
web-console/script/create-sql-docs.js | 19 +++--
web-console/src/bootstrap/json-parser.tsx | 24 ++++++
.../src/components/array-input/array-input.tsx | 30 +++----
web-console/src/entry.ts | 2 +
web-console/src/utils/general.spec.ts | 19 ++---
web-console/src/utils/general.tsx | 22 ++---
web-console/src/utils/sampler.ts | 17 +---
.../__snapshots__/ingestion-view.spec.tsx.snap | 1 +
.../src/views/ingestion-view/ingestion-view.tsx | 12 +++
.../src/views/load-data-view/info-messages.tsx | 7 +-
.../src/views/load-data-view/load-data-view.tsx | 97 ++++++++++++++++++----
14 files changed, 181 insertions(+), 79 deletions(-)
diff --git a/licenses.yaml b/licenses.yaml
index cf31a40..7375f5b 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -5164,7 +5164,7 @@ license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
-version: 0.11.6
+version: 0.11.10
---
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 1e009c8..5089227 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -7868,9 +7868,9 @@
}
},
"druid-query-toolkit": {
- "version": "0.11.6",
- "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.11.6.tgz",
- "integrity": "sha512-ThOhXW0CCEf08be+qpc4GwbSIewXZPoJViEAr0qx4s9B57vTIlz8VTdfrC0ei/r2PjfGNg+lx1RZAmvsbfo2tA==",
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.11.10.tgz",
+ "integrity": "sha512-jKqec2YMxCVvow8e9lmmrRKXxq/ugyeyKTVPaAUPbjoP4VHxk55BS2gXJ/S2ysCeVgvyJbjGbg2ZIkUzg4Whuw==",
"requires": {
"tslib": "^2.2.0"
}
diff --git a/web-console/package.json b/web-console/package.json
index f64366a..147e7eb 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -79,7 +79,7 @@
"d3-axis": "^1.0.12",
"d3-scale": "^3.2.0",
"d3-selection": "^1.4.0",
- "druid-query-toolkit": "^0.11.6",
+ "druid-query-toolkit": "^0.11.10",
"file-saver": "^2.0.2",
"fontsource-open-sans": "^3.0.9",
"has-own-prop": "^2.0.0",
diff --git a/web-console/script/create-sql-docs.js b/web-console/script/create-sql-docs.js
index 05abeb4..3e56864 100755
--- a/web-console/script/create-sql-docs.js
+++ b/web-console/script/create-sql-docs.js
@@ -23,6 +23,9 @@ const fs = require('fs-extra');
const readfile = '../docs/querying/sql.md';
const writefile = 'lib/sql-docs.js';
+const MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS = 134;
+const MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES = 14;
+
function unwrapMarkdownLinks(str) {
return str.replace(/\[([^\]]+)\]\([^)]+\)/g, (_, s) => s);
}
@@ -34,16 +37,17 @@ const readDoc = async () => {
const functionDocs = [];
const dataTypeDocs = [];
for (let line of lines) {
- const functionMatch = line.match(/^\|`(\w+)\((.*)\)`\|(.+)\|$/);
+ const functionMatch = line.match(/^\|`(\w+)\(([^|]*)\)`\|([^|]+)\|(?:([^|]+)\|)?$/);
if (functionMatch) {
functionDocs.push([
functionMatch[1],
functionMatch[2],
unwrapMarkdownLinks(functionMatch[3]),
+ // functionMatch[4] would be the default column but we ignore it for now
]);
}
- const dataTypeMatch = line.match(/^\|([A-Z]+)\|([A-Z]+)\|(.*)\|(.*)\|$/);
+ const dataTypeMatch = line.match(/^\|([A-Z]+)\|([A-Z]+)\|([^|]*)\|([^|]*)\|$/);
if (dataTypeMatch) {
dataTypeDocs.push([
dataTypeMatch[1],
@@ -53,17 +57,17 @@ const readDoc = async () => {
}
}
- // Make sure there are at least 10 functions for sanity
- if (functionDocs.length < 10) {
+ // Make sure there are enough functions found
+ if (functionDocs.length < MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS) {
throw new Error(
- `Did not find enough function entries did the structure of '${readfile}' change? (found ${functionDocs.length})`,
+ `Did not find enough function entries did the structure of '${readfile}' change? (found ${functionDocs.length} but expected at least ${MINIMUM_EXPECTED_NUMBER_OF_FUNCTIONS})`,
);
}
// Make sure there are at least 10 data types for sanity
- if (dataTypeDocs.length < 10) {
+ if (dataTypeDocs.length < MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES) {
throw new Error(
- `Did not find enough data type entries did the structure of '${readfile}' change? (found ${dataTypeDocs.length})`,
+ `Did not find enough data type entries did the structure of '${readfile}' change? (found ${dataTypeDocs.length} but expected at least ${MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES})`,
);
}
@@ -94,6 +98,7 @@ exports.SQL_DATA_TYPES = ${JSON.stringify(dataTypeDocs, null, 2)};
exports.SQL_FUNCTIONS = ${JSON.stringify(functionDocs, null, 2)};
`;
+ console.log(`Found ${dataTypeDocs.length} data types and ${functionDocs.length} functions`);
await fs.writeFile(writefile, content, 'utf-8');
};
diff --git a/web-console/src/bootstrap/json-parser.tsx b/web-console/src/bootstrap/json-parser.tsx
new file mode 100644
index 0000000..d8fd232
--- /dev/null
+++ b/web-console/src/bootstrap/json-parser.tsx
@@ -0,0 +1,24 @@
+/*
+ * 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 { QueryResult } from 'druid-query-toolkit';
+import * as JSONBig from 'json-bigint-native';
+
+export function bootstrapJsonParse() {
+ QueryResult.jsonParse = JSONBig.parse;
+}
diff --git a/web-console/src/components/array-input/array-input.tsx b/web-console/src/components/array-input/array-input.tsx
index 913949c..7ff376f 100644
--- a/web-console/src/components/array-input/array-input.tsx
+++ b/web-console/src/components/array-input/array-input.tsx
@@ -32,30 +32,30 @@ export interface ArrayInputProps {
}
export const ArrayInput = React.memo(function ArrayInput(props: ArrayInputProps) {
- const { className, placeholder, large, disabled, intent } = props;
- const [stringValue, setStringValue] = useState<string>();
+ const { className, placeholder, large, disabled, intent, onChange } = props;
+ const [intermediateValue, setIntermediateValue] = useState<string | undefined>();
const handleChange = (e: any) => {
- const { onChange } = props;
const stringValue = e.target.value;
- const newValues: string[] = stringValue.split(/[,\s]+/).map((v: string) => v.trim());
- const newValuesFiltered = compact(newValues);
- if (stringValue === '') {
- onChange(undefined);
- setStringValue(undefined);
- } else if (newValues.length === newValuesFiltered.length) {
- onChange(newValuesFiltered);
- setStringValue(undefined);
- } else {
- setStringValue(stringValue);
- }
+ setIntermediateValue(stringValue);
+
+ onChange(
+ stringValue === ''
+ ? undefined
+ : compact(stringValue.split(/[,\s]+/).map((v: string) => v.trim())),
+ );
};
return (
<TextArea
className={className}
- value={stringValue ?? props.values?.join(', ') ?? ''}
+ value={
+ typeof intermediateValue !== 'undefined'
+ ? intermediateValue
+ : props.values?.join(', ') || ''
+ }
onChange={handleChange}
+ onBlur={() => setIntermediateValue(undefined)}
placeholder={placeholder}
large={large}
disabled={disabled}
diff --git a/web-console/src/entry.ts b/web-console/src/entry.ts
index 088473d..64ff886 100644
--- a/web-console/src/entry.ts
+++ b/web-console/src/entry.ts
@@ -23,6 +23,7 @@ import './bootstrap/ace';
import React from 'react';
import ReactDOM from 'react-dom';
+import { bootstrapJsonParse } from './bootstrap/json-parser';
import { bootstrapReactTable } from './bootstrap/react-table-defaults';
import { ConsoleApplication } from './console-application';
import { Links, setLinkOverrides } from './links';
@@ -31,6 +32,7 @@ import { Api, UrlBaser } from './singletons';
import './entry.scss';
bootstrapReactTable();
+bootstrapJsonParse();
const container = document.getElementsByClassName('app-container')[0];
if (!container) throw new Error('container not found');
diff --git a/web-console/src/utils/general.spec.ts b/web-console/src/utils/general.spec.ts
index ea1078c..799fe8d 100644
--- a/web-console/src/utils/general.spec.ts
+++ b/web-console/src/utils/general.spec.ts
@@ -17,7 +17,7 @@
*/
import {
- alphanumericCompare,
+ arrangeWithPrefixSuffix,
formatBytes,
formatBytesCompact,
formatInteger,
@@ -25,33 +25,30 @@ import {
formatMillions,
formatPercent,
moveElement,
- sortWithPrefixSuffix,
sqlQueryCustomTableFilter,
swapElements,
} from './general';
describe('general', () => {
- describe('sortWithPrefixSuffix', () => {
+ describe('arrangeWithPrefixSuffix', () => {
it('works in simple case', () => {
expect(
- sortWithPrefixSuffix(
+ arrangeWithPrefixSuffix(
'abcdefgh'.split('').reverse(),
'gef'.split(''),
'ba'.split(''),
- alphanumericCompare,
).join(''),
- ).toEqual('gefcdhba');
+ ).toEqual('gefhdcba');
});
it('dedupes', () => {
expect(
- sortWithPrefixSuffix(
+ arrangeWithPrefixSuffix(
'abcdefgh'.split('').reverse(),
'gefgef'.split(''),
'baba'.split(''),
- alphanumericCompare,
).join(''),
- ).toEqual('gefcdhba');
+ ).toEqual('gefhdcba');
});
});
@@ -72,10 +69,10 @@ describe('general', () => {
String(
sqlQueryCustomTableFilter({
id: 'datasource',
- value: `"hello"`,
+ value: `"Hello"`,
}),
),
- ).toEqual(`"datasource" = 'hello'`);
+ ).toEqual(`"datasource" = 'Hello'`);
});
});
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 3557deb..e16e337 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -100,28 +100,29 @@ interface NeedleAndMode {
}
export function getNeedleAndMode(filter: Filter): NeedleAndMode {
- const input = filter.value.toLowerCase();
+ const input = filter.value;
if (input.startsWith(`"`) && input.endsWith(`"`)) {
return {
needle: input.slice(1, -1),
mode: 'exact',
};
+ } else {
+ return {
+ needle: input.replace(/^"/, '').toLowerCase(),
+ mode: 'includes',
+ };
}
- return {
- needle: input.startsWith(`"`) ? input.substring(1) : input,
- mode: 'includes',
- };
}
export function booleanCustomTableFilter(filter: Filter, value: any): boolean {
if (value == null) return false;
- const haystack = String(value).toLowerCase();
const needleAndMode: NeedleAndMode = getNeedleAndMode(filter);
const needle = needleAndMode.needle;
if (needleAndMode.mode === 'exact') {
- return needle === haystack;
+ return needle === String(value);
+ } else {
+ return String(value).toLowerCase().includes(needle);
}
- return haystack.includes(needle);
}
export function sqlQueryCustomTableFilter(filter: Filter): SqlExpression {
@@ -304,16 +305,15 @@ export function alphanumericCompare(a: string, b: string): number {
return String(a).localeCompare(b, undefined, { numeric: true });
}
-export function sortWithPrefixSuffix(
+export function arrangeWithPrefixSuffix(
things: readonly string[],
prefix: readonly string[],
suffix: readonly string[],
- cmp?: (a: string, b: string) => number,
): string[] {
const pre = uniq(prefix.filter(x => things.includes(x)));
const mid = things.filter(x => !prefix.includes(x) && !suffix.includes(x));
const post = uniq(suffix.filter(x => things.includes(x)));
- return pre.concat(cmp ? mid.sort(cmp) : mid, post);
+ return pre.concat(mid, post);
}
// ----------------------------
diff --git a/web-console/src/utils/sampler.ts b/web-console/src/utils/sampler.ts
index 0ba89d9..4999182 100644
--- a/web-console/src/utils/sampler.ts
+++ b/web-console/src/utils/sampler.ts
@@ -40,13 +40,7 @@ import {
import { Api } from '../singletons';
import { getDruidErrorMessage, queryDruidRune } from './druid-query';
-import {
- alphanumericCompare,
- EMPTY_ARRAY,
- filterMap,
- oneOf,
- sortWithPrefixSuffix,
-} from './general';
+import { arrangeWithPrefixSuffix, EMPTY_ARRAY, filterMap, oneOf } from './general';
import { deepGet, deepSet } from './object-change';
const SAMPLER_URL = `/druid/indexer/v1/sampler`;
@@ -153,11 +147,10 @@ export interface HeaderFromSampleResponseOptions {
export function headerFromSampleResponse(options: HeaderFromSampleResponseOptions): string[] {
const { sampleResponse, ignoreTimeColumn, columnOrder, suffixColumnOrder } = options;
- let columns = sortWithPrefixSuffix(
- dedupe(sampleResponse.data.flatMap(s => (s.parsed ? Object.keys(s.parsed) : []))).sort(),
+ let columns = arrangeWithPrefixSuffix(
+ dedupe(sampleResponse.data.flatMap(s => (s.parsed ? Object.keys(s.parsed) : []))),
columnOrder || [TIME_COLUMN],
suffixColumnOrder || [],
- alphanumericCompare,
);
if (ignoreTimeColumn) {
@@ -436,7 +429,6 @@ export async function sampleForTransform(
cacheRows: CacheRows,
): Promise<SampleResponse> {
const samplerType = getSpecType(spec);
- const inputFormatColumns: string[] = deepGet(spec, 'spec.ioConfig.inputFormat.columns') || [];
const timestampSpec: TimestampSpec = deepGet(spec, 'spec.dataSchema.timestampSpec');
const transforms: Transform[] = deepGet(spec, 'spec.dataSchema.transformSpec.transforms') || [];
@@ -465,7 +457,6 @@ export async function sampleForTransform(
headerFromSampleResponse({
sampleResponse: sampleResponseHack,
ignoreTimeColumn: true,
- columnOrder: [TIME_COLUMN].concat(inputFormatColumns),
}).concat(getDimensionNamesFromTransforms(transforms)),
);
}
@@ -494,7 +485,6 @@ export async function sampleForFilter(
cacheRows: CacheRows,
): Promise<SampleResponse> {
const samplerType = getSpecType(spec);
- const inputFormatColumns: string[] = deepGet(spec, 'spec.ioConfig.inputFormat.columns') || [];
const timestampSpec: TimestampSpec = deepGet(spec, 'spec.dataSchema.timestampSpec');
const transforms: Transform[] = deepGet(spec, 'spec.dataSchema.transformSpec.transforms') || [];
const filter: any = deepGet(spec, 'spec.dataSchema.transformSpec.filter');
@@ -524,7 +514,6 @@ export async function sampleForFilter(
headerFromSampleResponse({
sampleResponse: sampleResponseHack,
ignoreTimeColumn: true,
- columnOrder: [TIME_COLUMN].concat(inputFormatColumns),
}).concat(getDimensionNamesFromTransforms(transforms)),
);
}
diff --git a/web-console/src/views/ingestion-view/__snapshots__/ingestion-view.spec.tsx.snap b/web-console/src/views/ingestion-view/__snapshots__/ingestion-view.spec.tsx.snap
index 546b983..9cf646b 100644
--- a/web-console/src/views/ingestion-view/__snapshots__/ingestion-view.spec.tsx.snap
+++ b/web-console/src/views/ingestion-view/__snapshots__/ingestion-view.spec.tsx.snap
@@ -422,6 +422,7 @@ exports[`tasks view matches snapshot 1`] = `
},
Object {
"Aggregated": [Function],
+ "Cell": [Function],
"Header": "Group ID",
"accessor": "group_id",
"show": true,
diff --git a/web-console/src/views/ingestion-view/ingestion-view.tsx b/web-console/src/views/ingestion-view/ingestion-view.tsx
index 761db17..e28c449 100644
--- a/web-console/src/views/ingestion-view/ingestion-view.tsx
+++ b/web-console/src/views/ingestion-view/ingestion-view.tsx
@@ -749,6 +749,18 @@ ORDER BY "rank" DESC, "created_time" DESC`;
accessor: 'group_id',
width: 300,
Aggregated: () => '',
+ Cell: row => {
+ const value = row.value;
+ return (
+ <a
+ onClick={() => {
+ this.setState({ taskFilter: addFilter(taskFilter, 'group_id', value) });
+ }}
+ >
+ {value}
+ </a>
+ );
+ },
show: hiddenTaskColumns.exists('Group ID'),
},
{
diff --git a/web-console/src/views/load-data-view/info-messages.tsx b/web-console/src/views/load-data-view/info-messages.tsx
index a9c1c1e..ee8a360 100644
--- a/web-console/src/views/load-data-view/info-messages.tsx
+++ b/web-console/src/views/load-data-view/info-messages.tsx
@@ -46,7 +46,7 @@ export const ConnectMessage = React.memo(function ConnectMessage(props: ConnectM
{inlineMode ? (
<>
<p>To get started, please paste some data in the box to the left.</p>
- <p>Click "Apply" to verify your data with Druid.</p>
+ <p>Click "Apply" to verify your data with Druid.</p>
</>
) : (
<p>To get started, please specify what data you want to ingest.</p>
@@ -171,6 +171,11 @@ export const PartitionMessage = React.memo(function PartitionMessage() {
<FormGroup>
<Callout>
<p>Configure how Druid will partition data.</p>
+ <p>
+ Druid datasources are always partitioned by time into time chunks (
+ <Code>Primary partitioning</Code>), and each time chunk contains one or more segments (
+ <Code>Secondary partitioning</Code>).
+ </p>
<LearnMore href={`${getLink('DOCS')}/ingestion/index.html#partitioning`} />
</Callout>
</FormGroup>
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx b/web-console/src/views/load-data-view/load-data-view.tsx
index 6098ba9..b784faa 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -126,6 +126,7 @@ import {
import { getLink } from '../../links';
import { Api, AppToaster, UrlBaser } from '../../singletons';
import {
+ alphanumericCompare,
deepDelete,
deepGet,
deepSet,
@@ -1308,8 +1309,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
async queryForParser(initRun = false) {
const { spec, sampleStrategy } = this.state;
const ioConfig: IoConfig = deepGet(spec, 'spec.ioConfig') || EMPTY_OBJECT;
- const inputFormatColumns: string[] =
- deepGet(spec, 'spec.ioConfig.inputFormat.columns') || EMPTY_ARRAY;
let issue: string | undefined;
if (issueWithIoConfig(ioConfig)) {
@@ -1345,7 +1344,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
data: headerAndRowsFromSampleResponse({
sampleResponse,
ignoreTimeColumn: true,
- columnOrder: inputFormatColumns,
}),
lastData: parserQueryState.getSomeData(),
}),
@@ -1578,8 +1576,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
async queryForTimestamp(initRun = false) {
const { spec, cacheRows } = this.state;
- const inputFormatColumns: string[] =
- deepGet(spec, 'spec.ioConfig.inputFormat.columns') || EMPTY_ARRAY;
if (!cacheRows) {
this.setState(({ timestampQueryState }) => ({
@@ -1618,7 +1614,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
data: {
headerAndRows: headerAndRowsFromSampleResponse({
sampleResponse,
- columnOrder: [TIME_COLUMN].concat(inputFormatColumns),
}),
spec,
},
@@ -1773,8 +1768,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
async queryForTransform(initRun = false) {
const { spec, cacheRows } = this.state;
- const inputFormatColumns: string[] =
- deepGet(spec, 'spec.ioConfig.inputFormat.columns') || EMPTY_ARRAY;
if (!cacheRows) {
this.setState(({ transformQueryState }) => ({
@@ -1812,7 +1805,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
transformQueryState: new QueryState({
data: headerAndRowsFromSampleResponse({
sampleResponse,
- columnOrder: [TIME_COLUMN].concat(inputFormatColumns),
}),
lastData: transformQueryState.getSomeData(),
}),
@@ -1996,8 +1988,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
async queryForFilter(initRun = false) {
const { spec, cacheRows } = this.state;
- const inputFormatColumns: string[] =
- deepGet(spec, 'spec.ioConfig.inputFormat.columns') || EMPTY_ARRAY;
if (!cacheRows) {
this.setState(({ filterQueryState }) => ({
@@ -2030,7 +2020,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
filterQueryState: new QueryState({
data: headerAndRowsFromSampleResponse({
sampleResponse,
- columnOrder: [TIME_COLUMN].concat(inputFormatColumns),
parsedOnly: true,
}),
lastData: filterQueryState.getSomeData(),
@@ -2053,7 +2042,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
const headerAndRowsNoFilter = headerAndRowsFromSampleResponse({
sampleResponse: sampleResponseNoFilter,
- columnOrder: [TIME_COLUMN].concat(inputFormatColumns),
parsedOnly: true,
});
@@ -2134,7 +2122,25 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
)}
{this.renderColumnFilterControls()}
</div>
- {this.renderNextBar({})}
+ {this.renderNextBar({
+ onNextStep: () => {
+ if (!filterQueryState.data) return false;
+
+ let newSpec = spec;
+ if (!deepGet(newSpec, 'spec.dataSchema.dimensionsSpec')) {
+ const currentRollup = deepGet(newSpec, 'spec.dataSchema.granularitySpec.rollup');
+ newSpec = updateSchemaWithSample(
+ newSpec,
+ filterQueryState.data,
+ 'specific',
+ typeof currentRollup === 'boolean' ? currentRollup : DEFAULT_ROLLUP_SETTING,
+ );
+ }
+
+ this.updateSpec(newSpec);
+ return true;
+ },
+ })}
</>
);
}
@@ -2306,6 +2312,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
);
}
+ const schemaToolsMenu = this.renderSchemaToolsMenu();
return (
<>
<div className="main">{mainFill}</div>
@@ -2458,6 +2465,13 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
}}
/>
</FormGroup>
+ {schemaToolsMenu && (
+ <FormGroup>
+ <Popover2 content={schemaToolsMenu}>
+ <Button icon={IconNames.BUILD} />
+ </Popover2>
+ </FormGroup>
+ )}
</>
)}
{this.renderAutoDimensionControls()}
@@ -2473,6 +2487,59 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
);
}
+ private readonly renderSchemaToolsMenu = () => {
+ const { spec } = this.state;
+ const dimensions: DimensionSpec[] | undefined = deepGet(
+ spec,
+ `spec.dataSchema.dimensionsSpec.dimensions`,
+ );
+ const metrics: MetricSpec[] | undefined = deepGet(spec, `spec.dataSchema.metricsSpec`);
+
+ if (!dimensions && !metrics) return;
+ return (
+ <Menu>
+ {dimensions && (
+ <MenuItem
+ icon={IconNames.ARROWS_HORIZONTAL}
+ text="Order dimensions alphabetically"
+ onClick={() => {
+ if (!dimensions) return;
+ const newSpec = deepSet(
+ spec,
+ `spec.dataSchema.dimensionsSpec.dimensions`,
+ dimensions
+ .slice()
+ .sort((d1, d2) =>
+ alphanumericCompare(getDimensionSpecName(d1), getDimensionSpecName(d2)),
+ ),
+ );
+ this.updateSpec(newSpec);
+ }}
+ />
+ )}
+ {metrics && (
+ <MenuItem
+ icon={IconNames.ARROWS_HORIZONTAL}
+ text="Order metrics alphabetically"
+ onClick={() => {
+ if (!metrics) return;
+ const newSpec = deepSet(
+ spec,
+ `spec.dataSchema.metricsSpec`,
+ metrics
+ .slice()
+ .sort((m1, m2) =>
+ alphanumericCompare(getMetricSpecName(m1), getMetricSpecName(m2)),
+ ),
+ );
+ this.updateSpec(newSpec);
+ }}
+ />
+ )}
+ </Menu>
+ );
+ };
+
private readonly onAutoDimensionSelect = (selectedAutoDimension: string) => {
this.setState({
selectedAutoDimension,
@@ -2694,7 +2761,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
model={selectedDimensionSpec}
onChange={selectedDimensionSpec => this.setState({ selectedDimensionSpec })}
/>
- {reorderDimensionMenu && (
+ {selectedDimensionSpecIndex !== -1 && (
<FormGroup>
<Popover2 content={reorderDimensionMenu}>
<Button
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org