diff --git a/web-console/script/create-sql-docs.js b/web-console/script/create-sql-docs.js
index 71ac809915c..6e76041d201 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 f2f13a2820f..af4f82b902d 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> extends React.PureComponent
);
}
- private renderStringInput(field: Field, sanitize?: (str: string) => string): JSX.Element {
+ private renderStringInput(field: Field, sanitizer?: (str: string) => string): JSX.Element {
const { model, large, onFinalize } = this.props;
const { required, defaultValue, modelValue } = AutoForm.computeFieldValues(model, field);
return (
{
- if (sanitize && typeof v === 'string') v = sanitize(v);
this.fieldChange(field, v);
}}
onBlur={() => {
@@ -397,9 +398,7 @@ export class AutoForm> 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 1d954642954..5e3f725b524 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"
>
.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 c6113b21a8d..1130e7fa7e0 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 = (
-
+
);
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 672f5726649..00000000000
--- 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`] = `
-
-
-
-`;
-
-exports[`FormattedInputGroup matches snapshot with escaped value 1`] = `
-
-
-
-`;
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 5622906bfb3..00000000000
--- 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;
- 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();
-
- return (
- {
- 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 00000000000..27b677dd0bf
--- /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`] = `
+
+`;
+
+exports[`FormattedInput matches snapshot with escaped value 1`] = `
+
+`;
diff --git a/web-console/src/components/formatted-input/formatted-input.scss b/web-console/src/components/formatted-input/formatted-input.scss
new file mode 100644
index 00000000000..4369df8b5b1
--- /dev/null
+++ b/web-console/src/components/formatted-input/formatted-input.scss
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+.formatted-input {
+ position: relative;
+
+ & > .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 dfa6739ee8d..da74ec71115 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 = (
- {}} formatter={JSON_STRING_FORMATTER} />
+ {}} formatter={JSON_STRING_FORMATTER} />
);
const { container } = render(suggestibleInput);
@@ -35,7 +35,7 @@ describe('FormattedInputGroup', () => {
it('matches snapshot with escaped value', () => {
const suggestibleInput = (
- {}}
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 00000000000..fb3c0a7bd4a
--- /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;
+ 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();
+ const [isFocused, setIsFocused] = useState(false);
+
+ const issue: string | undefined = issueWithValue?.(value);
+ const showIssue = Boolean(!isFocused && issue);
+
+ return (
+
+
{
+ 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 && (
+
+
+
+ )}
+
+ );
+});
diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts
index 7b412960c5b..fdefa7cc34b 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 c062e63e682..c2546d62c62 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) && (
{
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 5686c8a7203..6db0dccc214 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`] = `
`;
exports[`SuggestibleInput matches snapshot with escaped value 1`] = `
`;
diff --git a/web-console/src/components/suggestible-input/suggestible-input.tsx b/web-console/src/components/suggestible-input/suggestible-input.tsx
index 710bc134ec2..6723bea53fb 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 {
+export interface SuggestibleInputProps extends Omit {
onFinalize?: () => void;
suggestions?: Suggestion[];
}
@@ -60,7 +57,7 @@ export const SuggestibleInput = React.memo(function SuggestibleInput(props: Sugg
}
return (
-
}
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":
+ A URI which specifies a directory (or other searchable resource) in which to search for files specified as a
+
+ file
+
+ ,
+
+ hdfs
+
+ ,
+
+ s3
+
+ , or
+
+
+ gs
+
+ path prefix.
+
,
+ "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":
- URI for the file of interest, specified as a file, hdfs, or s3 path
+ URI for the file of interest, specified as a
+
+ file
+
+ ,
+
+ hdfs
+
+ ,
+
+
+ s3
+
+ , or
+
+ gs
+
+ path
The URI prefix option is strictly better than URI and should be used instead
,
+ "issueWithValue": [Function],
"label": "URI (deprecated)",
"name": "extractionNamespace.uri",
"placeholder": "s3://bucket/some/key/prefix/lookups-01.gz",
@@ -154,10 +193,22 @@ 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",
},
@@ -190,18 +241,6 @@ exports[`LookupEditDialog matches snapshot 1`] = `
"placeholder": "(optional - defaults to the second column)",
"type": "string",
},
- 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",
@@ -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,
@@ -243,12 +276,6 @@ exports[`LookupEditDialog matches snapshot 1`] = `
"name": "extractionNamespace.connectorConfig.password",
"type": "string",
},
- Object {
- "defined": [Function],
- "info": "Should tables be created",
- "name": "extractionNamespace.connectorConfig.createTables",
- "type": "boolean",
- },
Object {
"defined": [Function],
"info":
@@ -264,7 +291,7 @@ exports[`LookupEditDialog matches snapshot 1`] = `
,
"name": "extractionNamespace.table",
- "placeholder": "some_lookup_table",
+ "placeholder": "lookup_table",
"required": true,
"type": "string",
},
@@ -283,7 +310,7 @@ exports[`LookupEditDialog matches snapshot 1`] = `
,
"name": "extractionNamespace.keyColumn",
- "placeholder": "my_key_value",
+ "placeholder": "key_column",
"required": true,
"type": "string",
},
@@ -302,28 +329,10 @@ exports[`LookupEditDialog matches snapshot 1`] = `
,
"name": "extractionNamespace.valueColumn",
- "placeholder": "my_column_value",
+ "placeholder": "value_column",
"required": true,
"type": "string",
},
- Object {
- "defined": [Function],
- "info":
-
- 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:
-
-
- SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE
-
-
- filter
-
-
- ,
- "name": "extractionNamespace.filter",
- "placeholder": "(optional)",
- "type": "string",
- },
Object {
"defined": [Function],
"info":
@@ -340,9 +349,42 @@ exports[`LookupEditDialog matches snapshot 1`] = `
,
"label": "Timestamp column",
"name": "extractionNamespace.tsColumn",
- "placeholder": "(optional)",
+ "placeholder": "timestamp_column (optional)",
"type": "string",
},
+ Object {
+ "defined": [Function],
+ "info":
+
+ 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:
+
+
+ SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE
+
+
+ filter
+
+
+ ,
+ "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],
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 561b1f0ceaf..882a454502a 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 ffd4e49a62c..fb70c6c72a4 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,
) => void;
- lookupName: string;
+ lookupId: string;
lookupTier: string;
lookupVersion: string;
lookupSpec: Partial;
@@ -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();
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}
>
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={
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)}
/>
)}
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 98840963b3d..9de130d09f3 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`] = `
[] = [
@@ -53,6 +54,13 @@ export const DIMENSION_SPEC_FIELDS: Field[] = [
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 27b8aac6480..8a7b6bb5bf0 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 20641f25732..130d0682a47 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 6f36eed1394..f781a21f4cd 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[] = [
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 0d8fa03f3be..d1ba0c8e936 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 c8eee2066e7..8ad389d9988 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[] = [
{
name: 'type',
@@ -74,7 +90,7 @@ export const LOOKUP_FIELDS: Field[] = [
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[] = [
// 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[] = [
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: (
+
+ A URI which specifies a directory (or other searchable resource) in which to search for
+ files specified as a file
, hdfs
, s3
, or{' '}
+ gs
path prefix.
+
+ ),
},
{
name: 'extractionNamespace.uri',
@@ -132,9 +155,13 @@ export const LOOKUP_FIELDS: Field[] = [
!deepGet(l, 'extractionNamespace.uriPrefix'),
required: l =>
!deepGet(l, 'extractionNamespace.uriPrefix') && !deepGet(l, 'extractionNamespace.uri'),
+ issueWithValue: issueWithUri,
info: (
<>
- URI for the file of interest, specified as a file, hdfs, or s3 path
+
+ URI for the file of interest, specified as a file
, hdfs
,{' '}
+ s3
, or gs
path
+
The URI prefix option is strictly better than URI and should be used instead
>
),
@@ -170,32 +197,35 @@ export const LOOKUP_FIELDS: Field[] = [
),
},
+ // 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[] = [
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[] = [
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',
@@ -284,16 +282,10 @@ export const LOOKUP_FIELDS: Field[] = [
defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
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[] = [
{
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[] = [
{
name: 'extractionNamespace.valueColumn',
type: 'string',
- placeholder: 'my_column_value',
+ placeholder: 'value_column',
defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
required: true,
info: (
@@ -347,10 +339,29 @@ export const LOOKUP_FIELDS: Field[] = [
>
),
},
+ {
+ name: 'extractionNamespace.tsColumn',
+ type: 'string',
+ label: 'Timestamp column',
+ placeholder: 'timestamp_column (optional)',
+ defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
+ info: (
+ <>
+
+ The column in table which contains when the key was updated. This will become the Value in
+ the SQL query:
+
+
+ SELECT keyColumn, valueColumn, tsColumn ? FROM namespace.table WHERE
+ filter
+
+ >
+ ),
+ },
{
name: 'extractionNamespace.filter',
type: 'string',
- placeholder: '(optional)',
+ placeholder: 'for_lookup = 1 (optional)',
defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
info: (
<>
@@ -365,24 +376,14 @@ export const LOOKUP_FIELDS: Field[] = [
>
),
},
+
{
- name: 'extractionNamespace.tsColumn',
- type: 'string',
- label: 'Timestamp column',
- placeholder: '(optional)',
- defined: l => deepGet(l, 'extractionNamespace.type') === 'jdbc',
- info: (
- <>
-
- The column in table which contains when the key was updated. This will become the Value in
- the SQL query:
-
-
- SELECT keyColumn, valueColumn, tsColumn ? FROM namespace.table WHERE
- filter
-
- >
- ),
+ 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
@@ -403,15 +404,54 @@ export const LOOKUP_FIELDS: Field[] = [
];
export function isLookupInvalid(
- lookupName: string | undefined,
+ lookupId: string | undefined,
lookupVersion: string | undefined,
lookupTier: string | undefined,
lookupSpec: Partial,
) {
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 2efbb00be7c..14e0276ccf6 100644
--- a/web-console/src/utils/formatter.ts
+++ b/web-console/src/utils/formatter.ts
@@ -35,7 +35,7 @@ const JSON_ESCAPES: Record = {
// The stringifier is just JSON minus the double quotes, the parser is much more forgiving
export const JSON_STRING_FORMATTER: Formatter = {
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 0ac487bdfc2..3c1a9c16edb 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/utils/sanitizers.ts b/web-console/src/utils/sanitizers.ts
new file mode 100644
index 00000000000..1fa401cdb66
--- /dev/null
+++ b/web-console/src/utils/sanitizers.ts
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+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 141273b02f8..9d96b97d2ed 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"
>
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 dd1d81662bc..d6768e8361f 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": "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": [Function],
+ "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 309da348fc3..d627f1977da 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;
@@ -114,9 +124,10 @@ export class LookupsView extends React.PureComponent[] = [];
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 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 (
+ <>
+ 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 (
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}
/>