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 &quot;Apply&quot to verify your data with Druid.</p>
+            <p>Click &quot;Apply&quot; 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