mirror of https://github.com/apache/druid.git
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:
parent
a09688862e
commit
e4ec3527a4
|
@ -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],
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
.bp3-form-content {
|
||||
position: relative;
|
||||
|
||||
& > .bp3-popover2-target {
|
||||
& > .info-popover {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 5px;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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}
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('LookupEditDialog', () => {
|
|||
onClose={() => {}}
|
||||
onSubmit={() => {}}
|
||||
onChange={() => {}}
|
||||
lookupName="test"
|
||||
lookupId="test"
|
||||
lookupTier="test"
|
||||
lookupVersion="test"
|
||||
lookupSpec={{ type: 'map', map: { a: 1 } }}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.</>,
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -28,3 +28,4 @@ export * from './object-change';
|
|||
export * from './query-cursor';
|
||||
export * from './query-manager';
|
||||
export * from './query-state';
|
||||
export * from './sanitizers';
|
||||
|
|
|
@ -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, '');
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue