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
This commit is contained in:
Vadim Ogievetsky 2021-08-30 14:36:23 -07:00 committed by GitHub
parent a09688862e
commit e4ec3527a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 699 additions and 396 deletions

View File

@ -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],

View File

@ -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':

View File

@ -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"

View File

@ -24,7 +24,7 @@
.bp3-form-content {
position: relative;
& > .bp3-popover2-target {
& > .info-popover {
position: absolute;
right: 0;
top: 5px;

View File

@ -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>
);

View File

@ -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>
`;

View File

@ -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}
/>
);
});

View File

@ -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>
`;

View File

@ -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;
}
}

View File

@ -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}

View File

@ -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>
);
});

View File

@ -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';

View File

@ -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]}

View File

@ -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>
`;

View File

@ -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}

View File

@ -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",
@ -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": <React.Fragment>
@ -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,28 +329,10 @@ exports[`LookupEditDialog matches snapshot 1`] = `
</p>
</React.Fragment>,
"name": "extractionNamespace.valueColumn",
"placeholder": "my_column_value",
"placeholder": "value_column",
"required": true,
"type": "string",
},
Object {
"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:
</p>
<p>
SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE
<strong>
filter
</strong>
</p>
</React.Fragment>,
"name": "extractionNamespace.filter",
"placeholder": "(optional)",
"type": "string",
},
Object {
"defined": [Function],
"info": <React.Fragment>
@ -340,9 +349,42 @@ exports[`LookupEditDialog matches snapshot 1`] = `
</React.Fragment>,
"label": "Timestamp column",
"name": "extractionNamespace.tsColumn",
"placeholder": "(optional)",
"placeholder": "timestamp_column (optional)",
"type": "string",
},
Object {
"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:
</p>
<p>
SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE
<strong>
filter
</strong>
</p>
</React.Fragment>,
"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],

View File

@ -28,7 +28,7 @@ describe('LookupEditDialog', () => {
onClose={() => {}}
onSubmit={() => {}}
onChange={() => {}}
lookupName="test"
lookupId="test"
lookupTier="test"
lookupVersion="test"
lookupSpec={{ type: 'map', map: { a: 1 } }}

View File

@ -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>

View File

@ -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"

View File

@ -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 {

View File

@ -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');
});
});
});

View File

@ -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;
}

View File

@ -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.</>,
},

View File

@ -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);

View File

@ -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',
@ -284,16 +282,10 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
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<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: (
@ -347,10 +339,29 @@ export const LOOKUP_FIELDS: Field<LookupSpec>[] = [
</>
),
},
{
name: 'extractionNamespace.tsColumn',
type: 'string',
label: 'Timestamp column',
placeholder: 'timestamp_column (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:
</p>
<p>
SELECT keyColumn, valueColumn, <strong>tsColumn</strong>? FROM namespace.table WHERE
filter
</p>
</>
),
},
{
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<LookupSpec>[] = [
</>
),
},
{
name: 'extractionNamespace.tsColumn',
type: 'string',
label: 'Timestamp column',
placeholder: '(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:
</p>
<p>
SELECT keyColumn, valueColumn, <strong>tsColumn</strong>? FROM namespace.table WHERE
filter
</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
@ -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';
}

View File

@ -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);

View File

@ -28,3 +28,4 @@ export * from './object-change';
export * from './query-cursor';
export * from './query-manager';
export * from './query-state';
export * from './sanitizers';

View File

@ -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, '');
}

View File

@ -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>

View File

@ -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,

View File

@ -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}
/>