You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by cw...@apache.org on 2021/08/30 21:36:45 UTC

[druid] branch master updated: Web console: Improve the lookup view UX (#11620)

This is an automated email from the ASF dual-hosted git repository.

cwylie 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 e4ec352  Web console: Improve the lookup view UX (#11620)
e4ec352 is described below

commit e4ec3527a4c41b56c2dc3fefea89a2469975aff3
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Aug 30 14:36:23 2021 -0700

    Web console: Improve the lookup view UX (#11620)
    
    * polish lookup view UX
    
    * update snapshots
    
    * add snapshot to git
    
    * fixes
    
    * update sanpshots
    
    * restore column treatment
    
    * update snapshot
    
    * add gs
---
 web-console/script/create-sql-docs.js              |   4 +-
 web-console/src/components/auto-form/auto-form.tsx |  11 +-
 .../form-group-with-info.spec.tsx.snap             |   2 +-
 .../form-group-with-info/form-group-with-info.scss |   2 +-
 .../form-group-with-info/form-group-with-info.tsx  |   2 +-
 .../formatted-input-group.spec.tsx.snap            |  25 ---
 .../formatted-input-group.tsx                      |  69 -------
 .../__snapshots__/formatted-input.spec.tsx.snap    |  33 ++++
 .../formatted-input.scss}                          |  20 +-
 .../formatted-input.spec.tsx}                      |   8 +-
 .../components/formatted-input/formatted-input.tsx | 107 +++++++++++
 web-console/src/components/index.ts                |   2 +-
 .../src/components/rule-editor/rule-editor.tsx     |   4 +-
 .../__snapshots__/suggestible-input.spec.tsx.snap  | 136 +++++++-------
 .../suggestible-input/suggestible-input.tsx        |   9 +-
 .../__snapshots__/lookup-edit-dialog.spec.tsx.snap | 138 +++++++++-----
 .../lookup-edit-dialog/lookup-edit-dialog.spec.tsx |   2 +-
 .../lookup-edit-dialog/lookup-edit-dialog.tsx      |  18 +-
 .../__snapshots__/retention-dialog.spec.tsx.snap   |  70 +++----
 web-console/src/druid-models/dimension-spec.ts     |   8 +
 .../src/druid-models/ingestion-spec.spec.ts        |  14 +-
 web-console/src/druid-models/ingestion-spec.tsx    |  18 +-
 web-console/src/druid-models/input-format.tsx      |   3 +-
 web-console/src/druid-models/lookup-spec.spec.ts   |   6 +-
 web-console/src/druid-models/lookup-spec.tsx       | 204 ++++++++++++---------
 web-console/src/utils/formatter.ts                 |   2 +-
 web-console/src/utils/index.tsx                    |   1 +
 .../sanitizers.ts}                                 |  16 +-
 .../__snapshots__/form-editor.spec.tsx.snap        |  20 +-
 .../__snapshots__/lookups-view.spec.tsx.snap       |  31 +++-
 .../src/views/lookups-view/lookups-view.tsx        |  74 ++++++--
 31 files changed, 648 insertions(+), 411 deletions(-)

diff --git a/web-console/script/create-sql-docs.js b/web-console/script/create-sql-docs.js
index 71ac809..6e76041 100755
--- a/web-console/script/create-sql-docs.js
+++ b/web-console/script/create-sql-docs.js
@@ -23,7 +23,7 @@ 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_FUNCTIONS = 152;
 const MINIMUM_EXPECTED_NUMBER_OF_DATA_TYPES = 14;
 
 function unwrapMarkdownLinks(str) {
@@ -41,7 +41,7 @@ const readDoc = async () => {
   const functionDocs = [];
   const dataTypeDocs = [];
   for (let line of lines) {
-    const functionMatch = line.match(/^\|`(\w+)\(([^|]*)\)`\|([^|]+)\|(?:([^|]+)\|)?$/);
+    const functionMatch = line.match(/^\|\s*`(\w+)\(([^|]*)\)`\s*\|([^|]+)\|(?:([^|]+)\|)?$/);
     if (functionMatch) {
       functionDocs.push([
         functionMatch[1],
diff --git a/web-console/src/components/auto-form/auto-form.tsx b/web-console/src/components/auto-form/auto-form.tsx
index f2f13a2..af4f82b 100644
--- a/web-console/src/components/auto-form/auto-form.tsx
+++ b/web-console/src/components/auto-form/auto-form.tsx
@@ -20,7 +20,7 @@ import { Button, ButtonGroup, FormGroup, Intent, NumericInput } from '@blueprint
 import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 
-import { deepDelete, deepGet, deepSet } from '../../utils';
+import { deepDelete, deepGet, deepSet, durationSanitizer } from '../../utils';
 import { ArrayInput } from '../array-input/array-input';
 import { FormGroupWithInfo } from '../form-group-with-info/form-group-with-info';
 import { IntervalInput } from '../interval-input/interval-input';
@@ -281,15 +281,16 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
     );
   }
 
-  private renderStringInput(field: Field<T>, sanitize?: (str: string) => string): JSX.Element {
+  private renderStringInput(field: Field<T>, sanitizer?: (str: string) => string): JSX.Element {
     const { model, large, onFinalize } = this.props;
     const { required, defaultValue, modelValue } = AutoForm.computeFieldValues(model, field);
 
     return (
       <SuggestibleInput
         value={modelValue != null ? modelValue : defaultValue || ''}
+        sanitizer={sanitizer}
+        issueWithValue={field.issueWithValue}
         onValueChange={v => {
-          if (sanitize && typeof v === 'string') v = sanitize(v);
           this.fieldChange(field, v);
         }}
         onBlur={() => {
@@ -397,9 +398,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
       case 'string':
         return this.renderStringInput(field);
       case 'duration':
-        return this.renderStringInput(field, (str: string) =>
-          str.toUpperCase().replace(/[^0-9PYMDTHS.,]/g, ''),
-        );
+        return this.renderStringInput(field, durationSanitizer);
       case 'boolean':
         return this.renderBooleanInput(field);
       case 'string-array':
diff --git a/web-console/src/components/form-group-with-info/__snapshots__/form-group-with-info.spec.tsx.snap b/web-console/src/components/form-group-with-info/__snapshots__/form-group-with-info.spec.tsx.snap
index 1d95464..5e3f725 100644
--- a/web-console/src/components/form-group-with-info/__snapshots__/form-group-with-info.spec.tsx.snap
+++ b/web-console/src/components/form-group-with-info/__snapshots__/form-group-with-info.spec.tsx.snap
@@ -13,7 +13,7 @@ exports[`form group with info matches snapshot 1`] = `
       class="bp3-text-muted"
     >
       <span
-        class="bp3-popover2-target"
+        class="info-popover bp3-popover2-target"
       >
         <span
           class="bp3-icon bp3-icon-info-sign"
diff --git a/web-console/src/components/form-group-with-info/form-group-with-info.scss b/web-console/src/components/form-group-with-info/form-group-with-info.scss
index 4d61773..40ac3cc 100644
--- a/web-console/src/components/form-group-with-info/form-group-with-info.scss
+++ b/web-console/src/components/form-group-with-info/form-group-with-info.scss
@@ -24,7 +24,7 @@
   .bp3-form-content {
     position: relative;
 
-    & > .bp3-popover2-target {
+    & > .info-popover {
       position: absolute;
       right: 0;
       top: 5px;
diff --git a/web-console/src/components/form-group-with-info/form-group-with-info.tsx b/web-console/src/components/form-group-with-info/form-group-with-info.tsx
index c6113b2..1130e7f 100644
--- a/web-console/src/components/form-group-with-info/form-group-with-info.tsx
+++ b/web-console/src/components/form-group-with-info/form-group-with-info.tsx
@@ -36,7 +36,7 @@ export const FormGroupWithInfo = React.memo(function FormGroupWithInfo(
   const { label, info, inlineInfo, children } = props;
 
   const popover = (
-    <Popover2 content={info} position="left-bottom">
+    <Popover2 className="info-popover" content={info} position="left-bottom">
       <Icon icon={IconNames.INFO_SIGN} iconSize={14} />
     </Popover2>
   );
diff --git a/web-console/src/components/formatted-input-group/__snapshots__/formatted-input-group.spec.tsx.snap b/web-console/src/components/formatted-input-group/__snapshots__/formatted-input-group.spec.tsx.snap
deleted file mode 100644
index 672f572..0000000
--- a/web-console/src/components/formatted-input-group/__snapshots__/formatted-input-group.spec.tsx.snap
+++ /dev/null
@@ -1,25 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`FormattedInputGroup matches snapshot on undefined value 1`] = `
-<div
-  class="bp3-input-group formatted-input-group"
->
-  <input
-    class="bp3-input"
-    type="text"
-    value=""
-  />
-</div>
-`;
-
-exports[`FormattedInputGroup matches snapshot with escaped value 1`] = `
-<div
-  class="bp3-input-group formatted-input-group"
->
-  <input
-    class="bp3-input"
-    type="text"
-    value="Here are some chars \\\\t\\\\r\\\\n lol"
-  />
-</div>
-`;
diff --git a/web-console/src/components/formatted-input-group/formatted-input-group.tsx b/web-console/src/components/formatted-input-group/formatted-input-group.tsx
deleted file mode 100644
index 5622906..0000000
--- a/web-console/src/components/formatted-input-group/formatted-input-group.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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 { InputGroup, InputGroupProps2 } from '@blueprintjs/core';
-import classNames from 'classnames';
-import React, { useState } from 'react';
-
-import { Formatter } from '../../utils';
-
-export interface FormattedInputGroupProps extends InputGroupProps2 {
-  formatter: Formatter<any>;
-  onValueChange: (newValue: undefined | string) => void;
-}
-
-export const FormattedInputGroup = React.memo(function FormattedInputGroup(
-  props: FormattedInputGroupProps,
-) {
-  const { className, formatter, value, defaultValue, onValueChange, onBlur, ...rest } = props;
-
-  const [intermediateValue, setIntermediateValue] = useState<string | undefined>();
-
-  return (
-    <InputGroup
-      className={classNames('formatted-input-group', className)}
-      value={
-        typeof intermediateValue !== 'undefined'
-          ? intermediateValue
-          : typeof value !== 'undefined'
-          ? formatter.stringify(value)
-          : undefined
-      }
-      defaultValue={
-        typeof defaultValue !== 'undefined' ? formatter.stringify(defaultValue) : undefined
-      }
-      onChange={e => {
-        const rawValue = e.target.value;
-        setIntermediateValue(rawValue);
-
-        let parsedValue: string | undefined;
-        try {
-          parsedValue = formatter.parse(rawValue);
-        } catch {
-          return;
-        }
-        onValueChange(parsedValue);
-      }}
-      onBlur={e => {
-        setIntermediateValue(undefined);
-        onBlur?.(e);
-      }}
-      {...rest}
-    />
-  );
-});
diff --git a/web-console/src/components/formatted-input/__snapshots__/formatted-input.spec.tsx.snap b/web-console/src/components/formatted-input/__snapshots__/formatted-input.spec.tsx.snap
new file mode 100644
index 0000000..27b677d
--- /dev/null
+++ b/web-console/src/components/formatted-input/__snapshots__/formatted-input.spec.tsx.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FormattedInput matches snapshot on undefined value 1`] = `
+<div
+  class="formatted-input"
+>
+  <div
+    class="bp3-input-group"
+  >
+    <input
+      class="bp3-input"
+      type="text"
+      value=""
+    />
+  </div>
+</div>
+`;
+
+exports[`FormattedInput matches snapshot with escaped value 1`] = `
+<div
+  class="formatted-input"
+>
+  <div
+    class="bp3-input-group"
+  >
+    <input
+      class="bp3-input"
+      type="text"
+      value="Here are some chars \\\\t\\\\r\\\\n lol"
+    />
+  </div>
+</div>
+`;
diff --git a/web-console/src/components/form-group-with-info/form-group-with-info.scss b/web-console/src/components/formatted-input/formatted-input.scss
similarity index 77%
copy from web-console/src/components/form-group-with-info/form-group-with-info.scss
copy to web-console/src/components/formatted-input/formatted-input.scss
index 4d61773..4369df8 100644
--- a/web-console/src/components/form-group-with-info/form-group-with-info.scss
+++ b/web-console/src/components/formatted-input/formatted-input.scss
@@ -16,18 +16,14 @@
  * limitations under the License.
  */
 
-.form-group-with-info {
-  .bp3-text-muted .bp3-popover2-target {
-    margin-top: 0;
-  }
-
-  .bp3-form-content {
-    position: relative;
+.formatted-input {
+  position: relative;
 
-    & > .bp3-popover2-target {
-      position: absolute;
-      right: 0;
-      top: 5px;
-    }
+  & > .bp3-popover2-target {
+    position: absolute;
+    width: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
   }
 }
diff --git a/web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx b/web-console/src/components/formatted-input/formatted-input.spec.tsx
similarity index 87%
rename from web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx
rename to web-console/src/components/formatted-input/formatted-input.spec.tsx
index dfa6739..da74ec7 100644
--- a/web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx
+++ b/web-console/src/components/formatted-input/formatted-input.spec.tsx
@@ -21,12 +21,12 @@ import React from 'react';
 
 import { JSON_STRING_FORMATTER } from '../../utils';
 
-import { FormattedInputGroup } from './formatted-input-group';
+import { FormattedInput } from './formatted-input';
 
-describe('FormattedInputGroup', () => {
+describe('FormattedInput', () => {
   it('matches snapshot on undefined value', () => {
     const suggestibleInput = (
-      <FormattedInputGroup onValueChange={() => {}} formatter={JSON_STRING_FORMATTER} />
+      <FormattedInput onValueChange={() => {}} formatter={JSON_STRING_FORMATTER} />
     );
 
     const { container } = render(suggestibleInput);
@@ -35,7 +35,7 @@ describe('FormattedInputGroup', () => {
 
   it('matches snapshot with escaped value', () => {
     const suggestibleInput = (
-      <FormattedInputGroup
+      <FormattedInput
         value={`Here are some chars \t\r\n lol`}
         onValueChange={() => {}}
         formatter={JSON_STRING_FORMATTER}
diff --git a/web-console/src/components/formatted-input/formatted-input.tsx b/web-console/src/components/formatted-input/formatted-input.tsx
new file mode 100644
index 0000000..fb3c0a7
--- /dev/null
+++ b/web-console/src/components/formatted-input/formatted-input.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { InputGroup, InputGroupProps2, Intent } from '@blueprintjs/core';
+import { Tooltip2 } from '@blueprintjs/popover2';
+import classNames from 'classnames';
+import React, { useState } from 'react';
+
+import { Formatter } from '../../utils';
+
+import './formatted-input.scss';
+
+export interface FormattedInputProps extends InputGroupProps2 {
+  formatter: Formatter<any>;
+  onValueChange: (newValue: undefined | string) => void;
+  sanitizer?: (rawValue: string) => string;
+  issueWithValue?: (value: any) => string | undefined;
+}
+
+export const FormattedInput = React.memo(function FormattedInput(props: FormattedInputProps) {
+  const {
+    className,
+    formatter,
+    sanitizer,
+    issueWithValue,
+    value,
+    defaultValue,
+    onValueChange,
+    onFocus,
+    onBlur,
+    intent,
+    ...rest
+  } = props;
+
+  const [intermediateValue, setIntermediateValue] = useState<string | undefined>();
+  const [isFocused, setIsFocused] = useState(false);
+
+  const issue: string | undefined = issueWithValue?.(value);
+  const showIssue = Boolean(!isFocused && issue);
+
+  return (
+    <div className={classNames('formatted-input', className)}>
+      <InputGroup
+        value={
+          typeof intermediateValue !== 'undefined'
+            ? intermediateValue
+            : typeof value !== 'undefined'
+            ? formatter.stringify(value)
+            : undefined
+        }
+        defaultValue={
+          typeof defaultValue !== 'undefined' ? formatter.stringify(defaultValue) : undefined
+        }
+        onChange={e => {
+          let rawValue = e.target.value;
+          if (sanitizer) rawValue = sanitizer(rawValue);
+          setIntermediateValue(rawValue);
+
+          let parsedValue: string | undefined;
+          try {
+            parsedValue = formatter.parse(rawValue);
+          } catch {
+            return;
+          }
+          onValueChange(parsedValue);
+        }}
+        onFocus={e => {
+          setIsFocused(true);
+          onFocus?.(e);
+        }}
+        onBlur={e => {
+          setIntermediateValue(undefined);
+          setIsFocused(false);
+          onBlur?.(e);
+        }}
+        intent={showIssue ? Intent.DANGER : intent}
+        {...rest}
+      />
+      {showIssue && (
+        <Tooltip2
+          isOpen
+          content={showIssue ? issue : undefined}
+          position="right"
+          intent={Intent.DANGER}
+          targetTagName="div"
+        >
+          <div className="target-dummy" />
+        </Tooltip2>
+      )}
+    </div>
+  );
+});
diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts
index 7b41296..fdefa7c 100644
--- a/web-console/src/components/index.ts
+++ b/web-console/src/components/index.ts
@@ -25,7 +25,7 @@ export * from './center-message/center-message';
 export * from './clearable-input/clearable-input';
 export * from './external-link/external-link';
 export * from './form-json-selector/form-json-selector';
-export * from './formatted-input-group/formatted-input-group';
+export * from './formatted-input/formatted-input';
 export * from './header-bar/header-bar';
 export * from './highlight-text/highlight-text';
 export * from './json-collapse/json-collapse';
diff --git a/web-console/src/components/rule-editor/rule-editor.tsx b/web-console/src/components/rule-editor/rule-editor.tsx
index c062e63..c2546d6 100644
--- a/web-console/src/components/rule-editor/rule-editor.tsx
+++ b/web-console/src/components/rule-editor/rule-editor.tsx
@@ -30,6 +30,7 @@ import {
 import { IconNames } from '@blueprintjs/icons';
 import React, { useState } from 'react';
 
+import { durationSanitizer } from '../../utils';
 import { Rule, RuleUtil } from '../../utils/load-rule';
 import { SuggestibleInput } from '../suggestible-input/suggestible-input';
 
@@ -175,10 +176,9 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps)
               {RuleUtil.hasPeriod(rule) && (
                 <SuggestibleInput
                   value={rule.period || ''}
+                  sanitizer={durationSanitizer}
                   onValueChange={period => {
                     if (typeof period === 'undefined') return;
-                    // Ensure the period is upper case and does not contain anytihng but the allowed chars
-                    period = period.toUpperCase().replace(/[^PYMDTHS0-9]/g, '');
                     onChange(RuleUtil.changePeriod(rule, period));
                   }}
                   placeholder={PERIOD_SUGGESTIONS[0]}
diff --git a/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap b/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap
index 5686c8a..6db0dcc 100644
--- a/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap
+++ b/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap
@@ -2,90 +2,98 @@
 
 exports[`SuggestibleInput matches snapshot 1`] = `
 <div
-  class="bp3-input-group formatted-input-group suggestible-input"
+  class="formatted-input suggestible-input"
 >
-  <input
-    class="bp3-input"
-    style="padding-right: 0px;"
-    type="text"
-    value=""
-  />
-  <span
-    class="bp3-input-action"
+  <div
+    class="bp3-input-group"
   >
+    <input
+      class="bp3-input"
+      style="padding-right: 0px;"
+      type="text"
+      value=""
+    />
     <span
-      class="bp3-popover2-target"
+      class="bp3-input-action"
     >
-      <button
-        class="bp3-button bp3-minimal"
-        type="button"
+      <span
+        class="bp3-popover2-target"
       >
-        <span
-          class="bp3-icon bp3-icon-caret-down"
-          icon="caret-down"
+        <button
+          class="bp3-button bp3-minimal"
+          type="button"
         >
-          <svg
-            data-icon="caret-down"
-            height="16"
-            viewBox="0 0 16 16"
-            width="16"
+          <span
+            class="bp3-icon bp3-icon-caret-down"
+            icon="caret-down"
           >
-            <desc>
-              caret-down
-            </desc>
-            <path
-              d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
-              fill-rule="evenodd"
-            />
-          </svg>
-        </span>
-      </button>
+            <svg
+              data-icon="caret-down"
+              height="16"
+              viewBox="0 0 16 16"
+              width="16"
+            >
+              <desc>
+                caret-down
+              </desc>
+              <path
+                d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
+                fill-rule="evenodd"
+              />
+            </svg>
+          </span>
+        </button>
+      </span>
     </span>
-  </span>
+  </div>
 </div>
 `;
 
 exports[`SuggestibleInput matches snapshot with escaped value 1`] = `
 <div
-  class="bp3-input-group formatted-input-group suggestible-input"
+  class="formatted-input suggestible-input"
 >
-  <input
-    class="bp3-input"
-    style="padding-right: 0px;"
-    type="text"
-    value="Here are some chars \\\\t\\\\r\\\\n lol"
-  />
-  <span
-    class="bp3-input-action"
+  <div
+    class="bp3-input-group"
   >
+    <input
+      class="bp3-input"
+      style="padding-right: 0px;"
+      type="text"
+      value="Here are some chars \\\\t\\\\r\\\\n lol"
+    />
     <span
-      class="bp3-popover2-target"
+      class="bp3-input-action"
     >
-      <button
-        class="bp3-button bp3-minimal"
-        type="button"
+      <span
+        class="bp3-popover2-target"
       >
-        <span
-          class="bp3-icon bp3-icon-caret-down"
-          icon="caret-down"
+        <button
+          class="bp3-button bp3-minimal"
+          type="button"
         >
-          <svg
-            data-icon="caret-down"
-            height="16"
-            viewBox="0 0 16 16"
-            width="16"
+          <span
+            class="bp3-icon bp3-icon-caret-down"
+            icon="caret-down"
           >
-            <desc>
-              caret-down
-            </desc>
-            <path
-              d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
-              fill-rule="evenodd"
-            />
-          </svg>
-        </span>
-      </button>
+            <svg
+              data-icon="caret-down"
+              height="16"
+              viewBox="0 0 16 16"
+              width="16"
+            >
+              <desc>
+                caret-down
+              </desc>
+              <path
+                d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
+                fill-rule="evenodd"
+              />
+            </svg>
+          </span>
+        </button>
+      </span>
     </span>
-  </span>
+  </div>
 </div>
 `;
diff --git a/web-console/src/components/suggestible-input/suggestible-input.tsx b/web-console/src/components/suggestible-input/suggestible-input.tsx
index 710bc13..6723bea 100644
--- a/web-console/src/components/suggestible-input/suggestible-input.tsx
+++ b/web-console/src/components/suggestible-input/suggestible-input.tsx
@@ -23,10 +23,7 @@ import classNames from 'classnames';
 import React, { useRef } from 'react';
 
 import { JSON_STRING_FORMATTER } from '../../utils';
-import {
-  FormattedInputGroup,
-  FormattedInputGroupProps,
-} from '../formatted-input-group/formatted-input-group';
+import { FormattedInput, FormattedInputProps } from '../formatted-input/formatted-input';
 
 export interface SuggestionGroup {
   group: string;
@@ -35,7 +32,7 @@ export interface SuggestionGroup {
 
 export type Suggestion = undefined | string | SuggestionGroup;
 
-export interface SuggestibleInputProps extends Omit<FormattedInputGroupProps, 'formatter'> {
+export interface SuggestibleInputProps extends Omit<FormattedInputProps, 'formatter'> {
   onFinalize?: () => void;
   suggestions?: Suggestion[];
 }
@@ -60,7 +57,7 @@ export const SuggestibleInput = React.memo(function SuggestibleInput(props: Sugg
   }
 
   return (
-    <FormattedInputGroup
+    <FormattedInput
       className={classNames('suggestible-input', className)}
       formatter={JSON_STRING_FORMATTER}
       value={value}
diff --git a/web-console/src/dialogs/lookup-edit-dialog/__snapshots__/lookup-edit-dialog.spec.tsx.snap b/web-console/src/dialogs/lookup-edit-dialog/__snapshots__/lookup-edit-dialog.spec.tsx.snap
index 14d7d58..4664ad2 100644
--- a/web-console/src/dialogs/lookup-edit-dialog/__snapshots__/lookup-edit-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/lookup-edit-dialog/__snapshots__/lookup-edit-dialog.spec.tsx.snap
@@ -2,6 +2,7 @@
 
 exports[`LookupEditDialog matches snapshot 1`] = `
 <Blueprint3.Dialog
+  canEscapeKeyClose={false}
   canOutsideClickClose={true}
   className="lookup-edit-dialog"
   isOpen={true}
@@ -53,7 +54,7 @@ exports[`LookupEditDialog matches snapshot 1`] = `
           <Blueprint3.Button
             minimal={true}
             onClick={[Function]}
-            text="Use ISO as version"
+            text="Set to current ISO time"
           />
         }
         value="test"
@@ -86,7 +87,7 @@ exports[`LookupEditDialog matches snapshot 1`] = `
           },
           Object {
             "defined": [Function],
-            "label": "Globally cached lookup type",
+            "label": "Extraction type",
             "name": "extractionNamespace.type",
             "placeholder": "uri",
             "required": true,
@@ -98,7 +99,27 @@ exports[`LookupEditDialog matches snapshot 1`] = `
           },
           Object {
             "defined": [Function],
-            "info": "A URI which specifies a directory (or other searchable resource) in which to search for files",
+            "info": <p>
+              A URI which specifies a directory (or other searchable resource) in which to search for files specified as a 
+              <Unknown>
+                file
+              </Unknown>
+              , 
+              <Unknown>
+                hdfs
+              </Unknown>
+              , 
+              <Unknown>
+                s3
+              </Unknown>
+              , or
+               
+              <Unknown>
+                gs
+              </Unknown>
+               path prefix.
+            </p>,
+            "issueWithValue": [Function],
             "label": "URI prefix",
             "name": "extractionNamespace.uriPrefix",
             "placeholder": "s3://bucket/some/key/prefix/",
@@ -109,12 +130,30 @@ exports[`LookupEditDialog matches snapshot 1`] = `
             "defined": [Function],
             "info": <React.Fragment>
               <p>
-                URI for the file of interest, specified as a file, hdfs, or s3 path
+                URI for the file of interest, specified as a 
+                <Unknown>
+                  file
+                </Unknown>
+                , 
+                <Unknown>
+                  hdfs
+                </Unknown>
+                ,
+                 
+                <Unknown>
+                  s3
+                </Unknown>
+                , or 
+                <Unknown>
+                  gs
+                </Unknown>
+                 path
               </p>
               <p>
                 The URI prefix option is strictly better than URI and should be used instead
               </p>
             </React.Fragment>,
+            "issueWithValue": [Function],
             "label": "URI (deprecated)",
             "name": "extractionNamespace.uri",
             "placeholder": "s3://bucket/some/key/prefix/lookups-01.gz",
@@ -155,9 +194,21 @@ exports[`LookupEditDialog matches snapshot 1`] = `
             "type": "string",
           },
           Object {
+            "defaultValue": "	",
+            "defined": [Function],
+            "name": "extractionNamespace.namespaceParseSpec.delimiter",
+            "suggestions": Array [
+              "	",
+              ";",
+              "|",
+              "#",
+            ],
+            "type": "string",
+          },
+          Object {
             "defaultValue": 0,
             "defined": [Function],
-            "info": "Number of header rows to be skipped. The default number of header rows to be skipped is 0.",
+            "info": "Number of header rows to be skipped.",
             "name": "extractionNamespace.namespaceParseSpec.skipHeaderRows",
             "type": "number",
           },
@@ -172,7 +223,7 @@ exports[`LookupEditDialog matches snapshot 1`] = `
             "defined": [Function],
             "info": "The list of columns in the csv file",
             "name": "extractionNamespace.namespaceParseSpec.columns",
-            "placeholder": "[\\"key\\", \\"value\\"]",
+            "placeholder": "key, value",
             "required": [Function],
             "type": "string-array",
           },
@@ -192,18 +243,6 @@ exports[`LookupEditDialog matches snapshot 1`] = `
           },
           Object {
             "defined": [Function],
-            "name": "extractionNamespace.namespaceParseSpec.delimiter",
-            "placeholder": "(optional)",
-            "type": "string",
-          },
-          Object {
-            "defined": [Function],
-            "name": "extractionNamespace.namespaceParseSpec.listDelimiter",
-            "placeholder": "(optional)",
-            "type": "string",
-          },
-          Object {
-            "defined": [Function],
             "name": "extractionNamespace.namespaceParseSpec.keyFieldName",
             "placeholder": "key",
             "required": true,
@@ -217,15 +256,9 @@ exports[`LookupEditDialog matches snapshot 1`] = `
             "type": "string",
           },
           Object {
-            "defaultValue": "0",
-            "defined": [Function],
-            "info": "Period between polling for updates",
-            "name": "extractionNamespace.pollPeriod",
-            "type": "string",
-          },
-          Object {
             "defined": [Function],
-            "info": "Defines the connectURI value on the The connector config to used",
+            "info": "Defines the connectURI for connecting to the database",
+            "issueWithValue": [Function],
             "label": "Connect URI",
             "name": "extractionNamespace.connectorConfig.connectURI",
             "required": true,
@@ -245,12 +278,6 @@ exports[`LookupEditDialog matches snapshot 1`] = `
           },
           Object {
             "defined": [Function],
-            "info": "Should tables be created",
-            "name": "extractionNamespace.connectorConfig.createTables",
-            "type": "boolean",
-          },
-          Object {
-            "defined": [Function],
             "info": <React.Fragment>
               <p>
                 The table which contains the key value pairs. This will become the table value in the SQL query:
@@ -264,7 +291,7 @@ exports[`LookupEditDialog matches snapshot 1`] = `
               </p>
             </React.Fragment>,
             "name": "extractionNamespace.table",
-            "placeholder": "some_lookup_table",
+            "placeholder": "lookup_table",
             "required": true,
             "type": "string",
           },
@@ -283,7 +310,7 @@ exports[`LookupEditDialog matches snapshot 1`] = `
               </p>
             </React.Fragment>,
             "name": "extractionNamespace.keyColumn",
-            "placeholder": "my_key_value",
+            "placeholder": "key_column",
             "required": true,
             "type": "string",
           },
@@ -302,7 +329,7 @@ exports[`LookupEditDialog matches snapshot 1`] = `
               </p>
             </React.Fragment>,
             "name": "extractionNamespace.valueColumn",
-            "placeholder": "my_column_value",
+            "placeholder": "value_column",
             "required": true,
             "type": "string",
           },
@@ -310,40 +337,55 @@ exports[`LookupEditDialog matches snapshot 1`] = `
             "defined": [Function],
             "info": <React.Fragment>
               <p>
-                The filter to be used when selecting lookups, this is used to create a where clause on lookup population. This will become the expression filter in the SQL query:
+                The column in table which contains when the key was updated. This will become the Value in the SQL query:
               </p>
               <p>
-                SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE
-                 
+                SELECT keyColumn, valueColumn, 
                 <strong>
-                  filter
+                  tsColumn
                 </strong>
+                ? FROM namespace.table WHERE filter
               </p>
             </React.Fragment>,
-            "name": "extractionNamespace.filter",
-            "placeholder": "(optional)",
+            "label": "Timestamp column",
+            "name": "extractionNamespace.tsColumn",
+            "placeholder": "timestamp_column (optional)",
             "type": "string",
           },
           Object {
             "defined": [Function],
             "info": <React.Fragment>
               <p>
-                The column in table which contains when the key was updated. This will become the Value in the SQL query:
+                The filter to be used when selecting lookups, this is used to create a where clause on lookup population. This will become the expression filter in the SQL query:
               </p>
               <p>
-                SELECT keyColumn, valueColumn, 
+                SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE
+                 
                 <strong>
-                  tsColumn
+                  filter
                 </strong>
-                ? FROM namespace.table WHERE filter
               </p>
             </React.Fragment>,
-            "label": "Timestamp column",
-            "name": "extractionNamespace.tsColumn",
-            "placeholder": "(optional)",
+            "name": "extractionNamespace.filter",
+            "placeholder": "for_lookup = 1 (optional)",
             "type": "string",
           },
           Object {
+            "defined": [Function],
+            "info": "Period between polling for updates",
+            "name": "extractionNamespace.pollPeriod",
+            "required": true,
+            "suggestions": Array [
+              "PT1M",
+              "PT10M",
+              "PT30M",
+              "PT1H",
+              "PT6H",
+              "P1D",
+            ],
+            "type": "duration",
+          },
+          Object {
             "defaultValue": 0,
             "defined": [Function],
             "info": "How long to wait (in ms) for the first run of the cache to populate. 0 indicates to not wait",
diff --git a/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.spec.tsx b/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.spec.tsx
index 561b1f0..882a454 100644
--- a/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.spec.tsx
+++ b/web-console/src/dialogs/lookup-edit-dialog/lookup-edit-dialog.spec.tsx
@@ -28,7 +28,7 @@ describe('LookupEditDialog', () => {
         onClose={() => {}}
         onSubmit={() => {}}
         onChange={() => {}}
-        lookupName="test"
+        lookupId="test"
         lookupTier="test"
         lookupVersion="test"
         lookupSpec={{ type: 'map', map: { a: 1 } }}
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 ffd4e49..fb70c6c 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
@@ -36,10 +36,10 @@ export interface LookupEditDialogProps {
   onClose: () => void;
   onSubmit: (updateLookupVersion: boolean) => void;
   onChange: (
-    field: 'name' | 'tier' | 'version' | 'spec',
+    field: 'id' | 'tier' | 'version' | 'spec',
     value: string | Partial<LookupSpec>,
   ) => void;
-  lookupName: string;
+  lookupId: string;
   lookupTier: string;
   lookupVersion: string;
   lookupSpec: Partial<LookupSpec>;
@@ -53,7 +53,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
     onSubmit,
     lookupSpec,
     lookupTier,
-    lookupName,
+    lookupId,
     lookupVersion,
     onChange,
     isEdit,
@@ -64,7 +64,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
   const [jsonError, setJsonError] = useState<Error | undefined>();
 
   const disableSubmit = Boolean(
-    jsonError || isLookupInvalid(lookupName, lookupVersion, lookupTier, lookupSpec),
+    jsonError || isLookupInvalid(lookupId, lookupVersion, lookupTier, lookupSpec),
   );
 
   return (
@@ -73,13 +73,14 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
       isOpen
       onClose={onClose}
       title={isEdit ? 'Edit lookup' : 'Add lookup'}
+      canEscapeKeyClose={false}
     >
       <div className="content">
         <FormGroup label="Name">
           <InputGroup
-            value={lookupName}
-            onChange={(e: any) => onChange('name', e.target.value)}
-            intent={lookupName ? Intent.NONE : Intent.PRIMARY}
+            value={lookupId}
+            onChange={(e: any) => onChange('id', e.target.value)}
+            intent={lookupId ? Intent.NONE : Intent.PRIMARY}
             disabled={isEdit}
             placeholder="Enter the lookup name"
           />
@@ -112,7 +113,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
             rightElement={
               <Button
                 minimal
-                text="Use ISO as version"
+                text="Set to current ISO time"
                 onClick={() => onChange('version', new Date().toISOString())}
               />
             }
@@ -136,6 +137,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
               setJsonError(undefined);
             }}
             onError={setJsonError}
+            issueWithValue={spec => AutoForm.issueWithModel(spec, LOOKUP_FIELDS)}
           />
         )}
       </div>
diff --git a/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap b/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
index 9884096..9de130d 100644
--- a/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
@@ -232,47 +232,51 @@ exports[`retention dialog matches snapshot 1`] = `
                               </span>
                             </div>
                             <div
-                              class="bp3-input-group formatted-input-group suggestible-input"
+                              class="formatted-input suggestible-input"
                             >
-                              <input
-                                class="bp3-input"
-                                placeholder="P1D"
-                                style="padding-right: 0px;"
-                                type="text"
-                                value="P1000Y"
-                              />
-                              <span
-                                class="bp3-input-action"
+                              <div
+                                class="bp3-input-group"
                               >
+                                <input
+                                  class="bp3-input"
+                                  placeholder="P1D"
+                                  style="padding-right: 0px;"
+                                  type="text"
+                                  value="P1000Y"
+                                />
                                 <span
-                                  class="bp3-popover2-target"
+                                  class="bp3-input-action"
                                 >
-                                  <button
-                                    class="bp3-button bp3-minimal"
-                                    type="button"
+                                  <span
+                                    class="bp3-popover2-target"
                                   >
-                                    <span
-                                      class="bp3-icon bp3-icon-caret-down"
-                                      icon="caret-down"
+                                    <button
+                                      class="bp3-button bp3-minimal"
+                                      type="button"
                                     >
-                                      <svg
-                                        data-icon="caret-down"
-                                        height="16"
-                                        viewBox="0 0 16 16"
-                                        width="16"
+                                      <span
+                                        class="bp3-icon bp3-icon-caret-down"
+                                        icon="caret-down"
                                       >
-                                        <desc>
-                                          caret-down
-                                        </desc>
-                                        <path
-                                          d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
-                                          fill-rule="evenodd"
-                                        />
-                                      </svg>
-                                    </span>
-                                  </button>
+                                        <svg
+                                          data-icon="caret-down"
+                                          height="16"
+                                          viewBox="0 0 16 16"
+                                          width="16"
+                                        >
+                                          <desc>
+                                            caret-down
+                                          </desc>
+                                          <path
+                                            d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
+                                            fill-rule="evenodd"
+                                          />
+                                        </svg>
+                                      </span>
+                                    </button>
+                                  </span>
                                 </span>
-                              </span>
+                              </div>
                             </div>
                             <label
                               class="bp3-control bp3-switch include-future"
diff --git a/web-console/src/druid-models/dimension-spec.ts b/web-console/src/druid-models/dimension-spec.ts
index 4805b5b..2859fb9 100644
--- a/web-console/src/druid-models/dimension-spec.ts
+++ b/web-console/src/druid-models/dimension-spec.ts
@@ -32,6 +32,7 @@ export interface DimensionSpec {
   readonly type: string;
   readonly name: string;
   readonly createBitmapIndex?: boolean;
+  readonly multiValueHandling?: string;
 }
 
 export const DIMENSION_SPEC_FIELDS: Field<DimensionSpec>[] = [
@@ -53,6 +54,13 @@ export const DIMENSION_SPEC_FIELDS: Field<DimensionSpec>[] = [
     defined: typeIs('string'),
     defaultValue: true,
   },
+  {
+    name: 'multiValueHandling',
+    type: 'string',
+    defined: typeIs('string'),
+    defaultValue: 'SORTED_ARRAY',
+    suggestions: ['SORTED_ARRAY', 'SORTED_SET', 'ARRAY'],
+  },
 ];
 
 export function getDimensionSpecName(dimensionSpec: string | DimensionSpec): string {
diff --git a/web-console/src/druid-models/ingestion-spec.spec.ts b/web-console/src/druid-models/ingestion-spec.spec.ts
index 27b8aac..8a7b6bb 100644
--- a/web-console/src/druid-models/ingestion-spec.spec.ts
+++ b/web-console/src/druid-models/ingestion-spec.spec.ts
@@ -156,8 +156,20 @@ describe('ingestion-spec', () => {
       expect(guessInputFormat(['A,B,X,Y']).type).toEqual('csv');
     });
 
+    it('works for TSV with ;', () => {
+      const inputFormat = guessInputFormat(['A;B;X;Y']);
+      expect(inputFormat.type).toEqual('tsv');
+      expect(inputFormat.delimiter).toEqual(';');
+    });
+
+    it('works for TSV with |', () => {
+      const inputFormat = guessInputFormat(['A|B|X|Y']);
+      expect(inputFormat.type).toEqual('tsv');
+      expect(inputFormat.delimiter).toEqual('|');
+    });
+
     it('works for regex', () => {
-      expect(guessInputFormat(['A|B|X|Y']).type).toEqual('regex');
+      expect(guessInputFormat(['A/B/X/Y']).type).toEqual('regex');
     });
   });
 });
diff --git a/web-console/src/druid-models/ingestion-spec.tsx b/web-console/src/druid-models/ingestion-spec.tsx
index 20641f2..130d068 100644
--- a/web-console/src/druid-models/ingestion-spec.tsx
+++ b/web-console/src/druid-models/ingestion-spec.tsx
@@ -2106,12 +2106,24 @@ export function guessInputFormat(sampleData: string[]): InputFormat {
     if (sampleDatum.split(',').length > 3) {
       return inputFormatFromType('csv', !/,\d+,/.test(sampleDatum));
     }
+    // Contains more than 3 semicolons assume semicolon separated
+    if (sampleDatum.split(';').length > 3) {
+      return inputFormatFromType('tsv', !/;\d+;/.test(sampleDatum), ';');
+    }
+    // Contains more than 3 pipes assume pipe separated
+    if (sampleDatum.split('|').length > 3) {
+      return inputFormatFromType('tsv', !/\|\d+\|/.test(sampleDatum), '|');
+    }
   }
 
   return inputFormatFromType('regex');
 }
 
-function inputFormatFromType(type: string, findColumnsFromHeader?: boolean): InputFormat {
+function inputFormatFromType(
+  type: string,
+  findColumnsFromHeader?: boolean,
+  delimiter?: string,
+): InputFormat {
   let inputFormat: InputFormat = { type };
 
   if (type === 'regex') {
@@ -2123,6 +2135,10 @@ function inputFormatFromType(type: string, findColumnsFromHeader?: boolean): Inp
     inputFormat = deepSet(inputFormat, 'findColumnsFromHeader', findColumnsFromHeader);
   }
 
+  if (delimiter) {
+    inputFormat = deepSet(inputFormat, 'delimiter', delimiter);
+  }
+
   return inputFormat;
 }
 
diff --git a/web-console/src/druid-models/input-format.tsx b/web-console/src/druid-models/input-format.tsx
index 6f36eed..f781a21 100644
--- a/web-console/src/druid-models/input-format.tsx
+++ b/web-console/src/druid-models/input-format.tsx
@@ -30,6 +30,7 @@ export interface InputFormat {
   readonly findColumnsFromHeader?: boolean;
   readonly skipHeaderRows?: number;
   readonly columns?: string[];
+  readonly delimiter?: string;
   readonly listDelimiter?: string;
   readonly pattern?: string;
   readonly function?: string;
@@ -113,7 +114,7 @@ export const INPUT_FORMAT_FIELDS: Field<InputFormat>[] = [
     name: 'delimiter',
     type: 'string',
     defaultValue: '\t',
-    suggestions: ['\t', '|', '#'],
+    suggestions: ['\t', ';', '|', '#'],
     defined: typeIs('tsv'),
     info: <>A custom delimiter for data values.</>,
   },
diff --git a/web-console/src/druid-models/lookup-spec.spec.ts b/web-console/src/druid-models/lookup-spec.spec.ts
index 0d8fa03..d1ba0c8 100644
--- a/web-console/src/druid-models/lookup-spec.spec.ts
+++ b/web-console/src/druid-models/lookup-spec.spec.ts
@@ -342,6 +342,7 @@ describe('lookup-spec', () => {
                 format: 'csv',
                 columns: ['key', 'value'],
               },
+              pollPeriod: 'PT1H',
             },
           }),
         ).toBe(false);
@@ -359,6 +360,7 @@ describe('lookup-spec', () => {
                 format: 'csv',
                 hasHeaderRow: true,
               },
+              pollPeriod: 'PT1H',
             },
           }),
         ).toBe(false);
@@ -376,6 +378,7 @@ describe('lookup-spec', () => {
                 format: 'tsv',
                 columns: ['key', 'value'],
               },
+              pollPeriod: 'PT1H',
             },
           }),
         ).toBe(false);
@@ -394,6 +397,7 @@ describe('lookup-spec', () => {
                 valueFieldName: 'value',
                 keyFieldName: 'value',
               },
+              pollPeriod: 'PT1H',
             },
           }),
         ).toBe(false);
@@ -416,7 +420,7 @@ describe('lookup-spec', () => {
               table: 'some_lookup_table',
               keyColumn: 'the_old_dim_value',
               valueColumn: 'the_new_dim_value',
-              pollPeriod: 600000,
+              pollPeriod: 'PT1H',
             },
           }),
         ).toBe(false);
diff --git a/web-console/src/druid-models/lookup-spec.tsx b/web-console/src/druid-models/lookup-spec.tsx
index c8eee20..8ad389d 100644
--- a/web-console/src/druid-models/lookup-spec.tsx
+++ b/web-console/src/druid-models/lookup-spec.tsx
@@ -20,7 +20,7 @@ import { Code } from '@blueprintjs/core';
 import React from 'react';
 
 import { AutoForm, Field } from '../components';
-import { deepGet, deepSet, oneOf, typeIs } from '../utils';
+import { deepGet, deepSet, oneOf, pluralIfNeeded, typeIs } from '../utils';
 
 export interface ExtractionNamespaceSpec {
   readonly type: string;
@@ -63,6 +63,22 @@ export interface LookupSpec {
   readonly injective?: boolean;
 }
 
+function issueWithUri(uri: string): string | undefined {
+  if (!uri) return;
+  const m = /^(\w+):/.exec(uri);
+  if (!m) return `URI is invalid, must start with 'file:', 'hdfs:', 's3:', or 'gs:`;
+  if (!oneOf(m[1], 'file', 'hdfs', 's3', 'gs')) {
+    return `Unsupported location '${m[1]}:'. Only 'file:', 'hdfs:', 's3:', and 'gs:' locations are supported`;
+  }
+  return;
+}
+
+function issueWithConnectUri(uri: string): string | undefined {
+  if (!uri) return;
+  if (!uri.startsWith('jdbc:')) return `connectURI is invalid, must start with 'jdbc:'`;
+  return;
+}
+
 export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
   {
     name: 'type',
@@ -74,7 +90,7 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
         return deepSet(l, 'map', {});
       }
       if (l.type === 'cachedNamespace' && !deepGet(l, 'extractionNamespace.type')) {
-        return deepSet(l, 'extractionNamespace', { type: 'uri' });
+        return deepSet(l, 'extractionNamespace', { type: 'uri', pollPeriod: 'PT1H' });
       }
       return l;
     },
@@ -103,13 +119,14 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
   // cachedNamespace lookups have more options
   {
     name: 'extractionNamespace.type',
-    label: 'Globally cached lookup type',
+    label: 'Extraction type',
     type: 'string',
     placeholder: 'uri',
     suggestions: ['uri', 'jdbc'],
     defined: typeIs('cachedNamespace'),
     required: true,
   },
+
   {
     name: 'extractionNamespace.uriPrefix',
     label: 'URI prefix',
@@ -119,8 +136,14 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
       deepGet(l, 'extractionNamespace.type') === 'uri' && !deepGet(l, 'extractionNamespace.uri'),
     required: l =>
       !deepGet(l, 'extractionNamespace.uriPrefix') && !deepGet(l, 'extractionNamespace.uri'),
-    info:
-      'A URI which specifies a directory (or other searchable resource) in which to search for files',
+    issueWithValue: issueWithUri,
+    info: (
+      <p>
+        A URI which specifies a directory (or other searchable resource) in which to search for
+        files specified as a <Code>file</Code>, <Code>hdfs</Code>, <Code>s3</Code>, or{' '}
+        <Code>gs</Code> path prefix.
+      </p>
+    ),
   },
   {
     name: 'extractionNamespace.uri',
@@ -132,9 +155,13 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
       !deepGet(l, 'extractionNamespace.uriPrefix'),
     required: l =>
       !deepGet(l, 'extractionNamespace.uriPrefix') && !deepGet(l, 'extractionNamespace.uri'),
+    issueWithValue: issueWithUri,
     info: (
       <>
-        <p>URI for the file of interest, specified as a file, hdfs, or s3 path</p>
+        <p>
+          URI for the file of interest, specified as a <Code>file</Code>, <Code>hdfs</Code>,{' '}
+          <Code>s3</Code>, or <Code>gs</Code> path
+        </p>
         <p>The URI prefix option is strictly better than URI and should be used instead</p>
       </>
     ),
@@ -170,32 +197,35 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
     ),
   },
 
+  // TSV only
+  {
+    name: 'extractionNamespace.namespaceParseSpec.delimiter',
+    type: 'string',
+    defaultValue: '\t',
+    suggestions: ['\t', ';', '|', '#'],
+    defined: l => deepGet(l, 'extractionNamespace.namespaceParseSpec.format') === 'tsv',
+  },
+
   // CSV + TSV
   {
     name: 'extractionNamespace.namespaceParseSpec.skipHeaderRows',
     type: 'number',
     defaultValue: 0,
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
-    info: `Number of header rows to be skipped. The default number of header rows to be skipped is 0.`,
+    defined: l => oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
+    info: `Number of header rows to be skipped.`,
   },
   {
     name: 'extractionNamespace.namespaceParseSpec.hasHeaderRow',
     type: 'boolean',
     defaultValue: false,
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
+    defined: l => oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
     info: `A flag to indicate that column information can be extracted from the input files' header row`,
   },
   {
     name: 'extractionNamespace.namespaceParseSpec.columns',
     type: 'string-array',
-    placeholder: `["key", "value"]`,
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
+    placeholder: 'key, value',
+    defined: l => oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
     required: l => !deepGet(l, 'extractionNamespace.namespaceParseSpec.hasHeaderRow'),
     info: 'The list of columns in the csv file',
   },
@@ -203,65 +233,32 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
     name: 'extractionNamespace.namespaceParseSpec.keyColumn',
     type: 'string',
     placeholder: '(optional - defaults to the first column)',
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
+    defined: l => oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
     info: 'The name of the column containing the key',
   },
   {
     name: 'extractionNamespace.namespaceParseSpec.valueColumn',
     type: 'string',
     placeholder: '(optional - defaults to the second column)',
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
+    defined: l => oneOf(deepGet(l, 'extractionNamespace.namespaceParseSpec.format'), 'csv', 'tsv'),
     info: 'The name of the column containing the value',
   },
 
-  // TSV only
-  {
-    name: 'extractionNamespace.namespaceParseSpec.delimiter',
-    type: 'string',
-    placeholder: `(optional)`,
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      deepGet(l, 'extractionNamespace.namespaceParseSpec.format') === 'tsv',
-  },
-  {
-    name: 'extractionNamespace.namespaceParseSpec.listDelimiter',
-    type: 'string',
-    placeholder: `(optional)`,
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      deepGet(l, 'extractionNamespace.namespaceParseSpec.format') === 'tsv',
-  },
-
   // Custom JSON
   {
     name: 'extractionNamespace.namespaceParseSpec.keyFieldName',
     type: 'string',
     placeholder: `key`,
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      deepGet(l, 'extractionNamespace.namespaceParseSpec.format') === 'customJson',
+    defined: l => deepGet(l, 'extractionNamespace.namespaceParseSpec.format') === 'customJson',
     required: true,
   },
   {
     name: 'extractionNamespace.namespaceParseSpec.valueFieldName',
     type: 'string',
     placeholder: `value`,
-    defined: l =>
-      deepGet(l, 'extractionNamespace.type') === 'uri' &&
-      deepGet(l, 'extractionNamespace.namespaceParseSpec.format') === 'customJson',
+    defined: l => deepGet(l, 'extractionNamespace.namespaceParseSpec.format') === 'customJson',
     required: true,
   },
-  {
-    name: 'extractionNamespace.pollPeriod',
-    type: 'string',
-    defaultValue: '0',
-    defined: l => oneOf(deepGet(l, 'extractionNamespace.type'), 'uri', 'jdbc'),
-    info: `Period between polling for updates`,
-  },
 
   // JDBC stuff
   {
@@ -270,7 +267,8 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
     type: 'string',
     defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
     required: true,
-    info: 'Defines the connectURI value on the The connector config to used',
+    issueWithValue: issueWithConnectUri,
+    info: 'Defines the connectURI for connecting to the database',
   },
   {
     name: 'extractionNamespace.connectorConfig.user',
@@ -285,15 +283,9 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
     info: 'Defines the password to be used by the connector config',
   },
   {
-    name: 'extractionNamespace.connectorConfig.createTables',
-    type: 'boolean',
-    defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
-    info: 'Should tables be created',
-  },
-  {
     name: 'extractionNamespace.table',
     type: 'string',
-    placeholder: 'some_lookup_table',
+    placeholder: 'lookup_table',
     defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
     required: true,
     info: (
@@ -312,7 +304,7 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
   {
     name: 'extractionNamespace.keyColumn',
     type: 'string',
-    placeholder: 'my_key_value',
+    placeholder: 'key_column',
     defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
     required: true,
     info: (
@@ -331,7 +323,7 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
   {
     name: 'extractionNamespace.valueColumn',
     type: 'string',
-    placeholder: 'my_column_value',
+    placeholder: 'value_column',
     defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
     required: true,
     info: (
@@ -348,43 +340,52 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
     ),
   },
   {
-    name: 'extractionNamespace.filter',
+    name: 'extractionNamespace.tsColumn',
     type: 'string',
-    placeholder: '(optional)',
+    label: 'Timestamp column',
+    placeholder: 'timestamp_column (optional)',
     defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
     info: (
       <>
         <p>
-          The filter to be used when selecting lookups, this is used to create a where clause on
-          lookup population. This will become the expression filter in the SQL query:
+          The column in table which contains when the key was updated. This will become the Value in
+          the SQL query:
         </p>
         <p>
-          SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE{' '}
-          <strong>filter</strong>
+          SELECT keyColumn, valueColumn, <strong>tsColumn</strong>? FROM namespace.table WHERE
+          filter
         </p>
       </>
     ),
   },
   {
-    name: 'extractionNamespace.tsColumn',
+    name: 'extractionNamespace.filter',
     type: 'string',
-    label: 'Timestamp column',
-    placeholder: '(optional)',
+    placeholder: 'for_lookup = 1 (optional)',
     defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
     info: (
       <>
         <p>
-          The column in table which contains when the key was updated. This will become the Value in
-          the SQL query:
+          The filter to be used when selecting lookups, this is used to create a where clause on
+          lookup population. This will become the expression filter in the SQL query:
         </p>
         <p>
-          SELECT keyColumn, valueColumn, <strong>tsColumn</strong>? FROM namespace.table WHERE
-          filter
+          SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE{' '}
+          <strong>filter</strong>
         </p>
       </>
     ),
   },
 
+  {
+    name: 'extractionNamespace.pollPeriod',
+    type: 'duration',
+    defined: l => oneOf(deepGet(l, 'extractionNamespace.type'), 'uri', 'jdbc'),
+    info: `Period between polling for updates`,
+    required: true,
+    suggestions: ['PT1M', 'PT10M', 'PT30M', 'PT1H', 'PT6H', 'P1D'],
+  },
+
   // Extra cachedNamespace things
   {
     name: 'firstCacheTimeout',
@@ -403,15 +404,54 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
 ];
 
 export function isLookupInvalid(
-  lookupName: string | undefined,
+  lookupId: string | undefined,
   lookupVersion: string | undefined,
   lookupTier: string | undefined,
   lookupSpec: Partial<LookupSpec>,
 ) {
   return (
-    !lookupName ||
-    !lookupVersion ||
-    !lookupTier ||
-    !AutoForm.isValidModel(lookupSpec, LOOKUP_FIELDS)
+    !lookupId || !lookupVersion || !lookupTier || !AutoForm.isValidModel(lookupSpec, LOOKUP_FIELDS)
   );
 }
+
+export function lookupSpecSummary(spec: LookupSpec): string {
+  const { map, extractionNamespace } = spec;
+
+  if (map) {
+    return pluralIfNeeded(Object.keys(map).length, 'key');
+  }
+
+  if (extractionNamespace) {
+    switch (extractionNamespace.type) {
+      case 'uri':
+        if (extractionNamespace.uriPrefix) {
+          return `URI prefix: ${extractionNamespace.uriPrefix}, Match: ${
+            extractionNamespace.fileRegex || '.*'
+          }`;
+        }
+        if (extractionNamespace.uri) {
+          return `URI: ${extractionNamespace.uri}`;
+        }
+        return 'Unknown extractionNamespace lookup';
+
+      case 'jdbc': {
+        const columns = [
+          `${extractionNamespace.keyColumn} AS key`,
+          `${extractionNamespace.valueColumn} AS value`,
+        ];
+        if (extractionNamespace.tsColumn) {
+          columns.push(`${extractionNamespace.tsColumn} AS ts`);
+        }
+        const queryParts = ['SELECT', columns.join(', '), `FROM ${extractionNamespace.table}`];
+        if (extractionNamespace.filter) {
+          queryParts.push(`WHERE ${extractionNamespace.filter}`);
+        }
+        return `${
+          extractionNamespace.connectorConfig?.connectURI || 'No connectURI'
+        } [${queryParts.join(' ')}]`;
+      }
+    }
+  }
+
+  return 'Unknown lookup';
+}
diff --git a/web-console/src/utils/formatter.ts b/web-console/src/utils/formatter.ts
index 2efbb00..14e0276 100644
--- a/web-console/src/utils/formatter.ts
+++ b/web-console/src/utils/formatter.ts
@@ -35,7 +35,7 @@ const JSON_ESCAPES: Record<string, string> = {
 // The stringifier is just JSON minus the double quotes, the parser is much more forgiving
 export const JSON_STRING_FORMATTER: Formatter<string> = {
   stringify: (str: string) => {
-    if (typeof str !== 'string') throw new TypeError(`must be a string`);
+    if (typeof str !== 'string') return '';
 
     const json = JSON.stringify(str);
     return json.substr(1, json.length - 2);
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index 0ac487b..3c1a9c1 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -28,3 +28,4 @@ export * from './object-change';
 export * from './query-cursor';
 export * from './query-manager';
 export * from './query-state';
+export * from './sanitizers';
diff --git a/web-console/src/components/form-group-with-info/form-group-with-info.scss b/web-console/src/utils/sanitizers.ts
similarity index 77%
copy from web-console/src/components/form-group-with-info/form-group-with-info.scss
copy to web-console/src/utils/sanitizers.ts
index 4d61773..1fa401c 100644
--- a/web-console/src/components/form-group-with-info/form-group-with-info.scss
+++ b/web-console/src/utils/sanitizers.ts
@@ -16,18 +16,6 @@
  * limitations under the License.
  */
 
-.form-group-with-info {
-  .bp3-text-muted .bp3-popover2-target {
-    margin-top: 0;
-  }
-
-  .bp3-form-content {
-    position: relative;
-
-    & > .bp3-popover2-target {
-      position: absolute;
-      right: 0;
-      top: 5px;
-    }
-  }
+export function durationSanitizer(str: string): string {
+  return str.toUpperCase().replace(/[^0-9PYMDTHS.,]/g, '');
 }
diff --git a/web-console/src/views/load-data-view/form-editor/__snapshots__/form-editor.spec.tsx.snap b/web-console/src/views/load-data-view/form-editor/__snapshots__/form-editor.spec.tsx.snap
index 141273b..9d96b97 100644
--- a/web-console/src/views/load-data-view/form-editor/__snapshots__/form-editor.spec.tsx.snap
+++ b/web-console/src/views/load-data-view/form-editor/__snapshots__/form-editor.spec.tsx.snap
@@ -19,7 +19,7 @@ exports[`FormEditor matches snapshot 1`] = `
           class="bp3-text-muted"
         >
           <span
-            class="bp3-popover2-target"
+            class="info-popover bp3-popover2-target"
           >
             <span
               class="bp3-icon bp3-icon-info-sign"
@@ -47,14 +47,18 @@ exports[`FormEditor matches snapshot 1`] = `
         class="bp3-form-content"
       >
         <div
-          class="bp3-input-group bp3-intent-primary formatted-input-group suggestible-input"
+          class="formatted-input suggestible-input"
         >
-          <input
-            class="bp3-input"
-            placeholder=""
-            type="text"
-            value=""
-          />
+          <div
+            class="bp3-input-group bp3-intent-primary"
+          >
+            <input
+              class="bp3-input"
+              placeholder=""
+              type="text"
+              value=""
+            />
+          </div>
         </div>
       </div>
     </div>
diff --git a/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap b/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
index dd1d816..d6768e8 100755
--- a/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
+++ b/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
@@ -23,6 +23,8 @@ exports[`lookups view matches snapshot 1`] = `
           "Lookup tier",
           "Type",
           "Version",
+          "Poll period",
+          "Summary",
           "Actions",
         ]
       }
@@ -93,6 +95,7 @@ exports[`lookups view matches snapshot 1`] = `
           "filterable": true,
           "id": "lookup_name",
           "show": true,
+          "width": 200,
         },
         Object {
           "Header": "Lookup tier",
@@ -100,6 +103,7 @@ exports[`lookups view matches snapshot 1`] = `
           "filterable": true,
           "id": "tier",
           "show": true,
+          "width": 100,
         },
         Object {
           "Header": "Type",
@@ -107,6 +111,7 @@ exports[`lookups view matches snapshot 1`] = `
           "filterable": true,
           "id": "type",
           "show": true,
+          "width": 150,
         },
         Object {
           "Header": "Version",
@@ -114,11 +119,26 @@ exports[`lookups view matches snapshot 1`] = `
           "filterable": true,
           "id": "version",
           "show": true,
+          "width": 190,
         },
         Object {
           "Cell": [Function],
-          "Header": "Actions",
+          "Header": "Poll period",
+          "accessor": [Function],
+          "id": "poolPeriod",
+          "show": true,
+          "width": 150,
+        },
+        Object {
+          "Header": "Summary",
           "accessor": [Function],
+          "id": "summary",
+          "show": true,
+        },
+        Object {
+          "Cell": [Function],
+          "Header": "Actions",
+          "accessor": "id",
           "filterable": false,
           "id": "actions",
           "show": true,
@@ -135,7 +155,14 @@ exports[`lookups view matches snapshot 1`] = `
     defaultResized={Array []}
     defaultSortDesc={false}
     defaultSortMethod={[Function]}
-    defaultSorted={Array []}
+    defaultSorted={
+      Array [
+        Object {
+          "desc": false,
+          "id": "lookup_name",
+        },
+      ]
+    }
     expanderDefaults={
       Object {
         "filterable": false,
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx b/web-console/src/views/lookups-view/lookups-view.tsx
index 309da34..d627f19 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Button, Intent } from '@blueprintjs/core';
+import { Button, Icon, Intent } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 import ReactTable from 'react-table';
@@ -32,9 +32,10 @@ import {
 } from '../../components';
 import { AsyncActionDialog, LookupEditDialog } from '../../dialogs/';
 import { LookupTableActionDialog } from '../../dialogs/lookup-table-action-dialog/lookup-table-action-dialog';
-import { LookupSpec } from '../../druid-models';
+import { LookupSpec, lookupSpecSummary } from '../../druid-models';
 import { Api, AppToaster } from '../../singletons';
 import {
+  deepGet,
   getDruidErrorMessage,
   isLookupsUninitialized,
   LocalStorageKeys,
@@ -51,6 +52,8 @@ const tableColumns: string[] = [
   'Lookup tier',
   'Type',
   'Version',
+  'Poll period',
+  'Summary',
   ACTION_COLUMN_LABEL,
 ];
 
@@ -61,12 +64,19 @@ function tierNameCompare(a: string, b: string) {
 }
 
 export interface LookupEntriesAndTiers {
-  lookupEntries: any[];
+  lookupEntries: LookupEntry[];
   tiers: string[];
 }
 
+export interface LookupEntry {
+  id: string;
+  tier: string;
+  version: string;
+  spec: LookupSpec;
+}
+
 export interface LookupEditInfo {
-  name: string;
+  id: string;
   tier: string;
   version: string;
   spec: Partial<LookupSpec>;
@@ -114,9 +124,10 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
             ? tiersResp.data.sort(tierNameCompare)
             : [DEFAULT_LOOKUP_TIER];
 
-        const lookupEntries: Record<string, string>[] = [];
         const lookupResp = await Api.instance.get('/druid/coordinator/v1/lookups/config/all');
         const lookupData = lookupResp.data;
+
+        const lookupEntries: LookupEntry[] = [];
         Object.keys(lookupData).map((tier: string) => {
           const lookupIds = lookupData[tier];
           Object.keys(lookupIds).map((id: string) => {
@@ -178,7 +189,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
         return {
           isEdit: false,
           lookupEdit: {
-            name: '',
+            id: '',
             tier: loadingEntriesAndTiers ? loadingEntriesAndTiers.tiers[0] : '',
             spec: { type: 'map', map: {} },
             version: new Date().toISOString(),
@@ -189,8 +200,8 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
       this.setState({
         isEdit: true,
         lookupEdit: {
-          name: id,
-          tier: tier,
+          id,
+          tier,
           spec: target.spec,
           version: target.version,
         },
@@ -216,7 +227,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
     const specJson: any = lookupEdit.spec;
     let dataJson: any;
     if (isEdit) {
-      endpoint = `${endpoint}/${lookupEdit.tier}/${lookupEdit.name}`;
+      endpoint = `${endpoint}/${lookupEdit.tier}/${lookupEdit.id}`;
       dataJson = {
         version: version,
         lookupExtractorFactory: specJson,
@@ -224,7 +235,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
     } else {
       dataJson = {
         [lookupEdit.tier]: {
-          [lookupEdit.name]: {
+          [lookupEdit.id]: {
             version: version,
             lookupExtractorFactory: specJson,
           },
@@ -319,6 +330,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
               : lookupEntriesAndTiersState.getErrorMessage() || ''
           }
           filterable
+          defaultSorted={[{ id: 'lookup_name', desc: false }]}
           columns={[
             {
               Header: 'Lookup name',
@@ -326,6 +338,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
               id: 'lookup_name',
               accessor: 'id',
               filterable: true,
+              width: 200,
             },
             {
               Header: 'Lookup tier',
@@ -333,6 +346,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
               id: 'tier',
               accessor: 'tier',
               filterable: true,
+              width: 100,
             },
             {
               Header: 'Type',
@@ -340,6 +354,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
               id: 'type',
               accessor: 'spec.type',
               filterable: true,
+              width: 150,
             },
             {
               Header: 'Version',
@@ -347,17 +362,44 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
               id: 'version',
               accessor: 'version',
               filterable: true,
+              width: 190,
+            },
+            {
+              Header: 'Poll period',
+              show: hiddenColumns.exists('Poll period'),
+              id: 'poolPeriod',
+              width: 150,
+              accessor: row => deepGet(row, 'spec.extractionNamespace.pollPeriod'),
+              Cell: ({ original }) => {
+                if (original.spec.type === 'map') return 'Static map';
+                const pollPeriod = deepGet(original, 'spec.extractionNamespace.pollPeriod');
+                if (!pollPeriod) {
+                  return (
+                    <>
+                      <Icon icon={IconNames.WARNING_SIGN} intent={Intent.WARNING} /> No poll period
+                      set
+                    </>
+                  );
+                }
+                return pollPeriod;
+              },
+            },
+            {
+              Header: 'Summary',
+              show: hiddenColumns.exists('Summary'),
+              id: 'summary',
+              accessor: row => lookupSpecSummary(row.spec),
             },
             {
               Header: ACTION_COLUMN_LABEL,
               show: hiddenColumns.exists(ACTION_COLUMN_LABEL),
               id: ACTION_COLUMN_ID,
               width: ACTION_COLUMN_WIDTH,
-              accessor: (row: any) => ({ id: row.id, tier: row.tier }),
               filterable: false,
-              Cell: (row: any) => {
-                const lookupId = row.value.id;
-                const lookupTier = row.value.tier;
+              accessor: 'id',
+              Cell: ({ original }) => {
+                const lookupId = original.id;
+                const lookupTier = original.tier;
                 const lookupActions = this.getLookupActions(lookupTier, lookupId);
                 return (
                   <ActionCell
@@ -391,10 +433,10 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
         onClose={() => this.setState({ lookupEdit: undefined })}
         onSubmit={updateLookupVersion => this.submitLookupEdit(updateLookupVersion)}
         onChange={this.handleChangeLookup}
-        lookupSpec={lookupEdit.spec}
-        lookupName={lookupEdit.name}
+        lookupId={lookupEdit.id}
         lookupTier={lookupEdit.tier}
         lookupVersion={lookupEdit.version}
+        lookupSpec={lookupEdit.spec}
         isEdit={isEdit}
         allLookupTiers={allLookupTiers}
       />

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org