Use auto-form for add an edit lookups (#9587)

* use auto form

* jest -u

* fix unreachable statment

* complete the owl

* jest -u

* remove changes to query-view

* fix permissions

* add test, fix info

* add cool highlights

* fix formatting

* fix capitalization

* add optional placeholder

* add space
This commit is contained in:
mcbrewster 2020-04-08 17:34:59 -06:00 committed by GitHub
parent 2b2b9efcd7
commit 6f3d403491
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 1104 additions and 142 deletions

View File

@ -58,7 +58,7 @@ exports[`lookup edit dialog matches snapshot 1`] = `
<label <label
class="bp3-label" class="bp3-label"
> >
Name: Name
<span <span
class="bp3-text-muted" class="bp3-text-muted"
@ -86,7 +86,7 @@ exports[`lookup edit dialog matches snapshot 1`] = `
<label <label
class="bp3-label" class="bp3-label"
> >
Tier: Tier
<span <span
class="bp3-text-muted" class="bp3-text-muted"
@ -178,7 +178,7 @@ exports[`lookup edit dialog matches snapshot 1`] = `
<label <label
class="bp3-label" class="bp3-label"
> >
Version: Version
<span <span
class="bp3-text-muted" class="bp3-text-muted"
@ -215,12 +215,15 @@ exports[`lookup edit dialog matches snapshot 1`] = `
</div> </div>
</div> </div>
<div <div
class="bp3-form-group lookup-label" class="auto-form"
>
<div
class="bp3-form-group form-group-with-info"
> >
<label <label
class="bp3-label" class="bp3-label"
> >
Spec: Type
<span <span
class="bp3-text-muted" class="bp3-text-muted"
@ -228,12 +231,76 @@ exports[`lookup edit dialog matches snapshot 1`] = `
</label> </label>
<div <div
class="bp3-form-content" class="bp3-form-content"
>
<div
class="bp3-input-group suggestible-input"
>
<input
class="bp3-input"
placeholder=""
style="padding-right: 0px;"
suggestions="map,cachedNamespace"
type="text"
value="map"
/> />
<span
class="bp3-input-action"
>
<span
class="bp3-popover-wrapper"
>
<span
class="bp3-popover-target"
>
<button
class="bp3-button bp3-minimal"
type="button"
>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<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>
</div> </div>
<div <div
class=" ace_editor ace-tm lookup-edit-dialog-textarea" class="bp3-form-group form-group-with-info"
>
<label
class="bp3-label"
>
Map
<span
class="bp3-text-muted"
/>
</label>
<div
class="bp3-form-content"
>
<div
class=" ace_editor ace-tm json-input"
id="brace-editor" id="brace-editor"
style="width: auto; height: 40vh;" style="width: 100%; height: 8vh;"
> >
<textarea <textarea
autocapitalize="off" autocapitalize="off"
@ -319,6 +386,9 @@ exports[`lookup edit dialog matches snapshot 1`] = `
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div>
<div <div
class="bp3-dialog-footer" class="bp3-dialog-footer"
> >
@ -336,9 +406,7 @@ exports[`lookup edit dialog matches snapshot 1`] = `
</span> </span>
</button> </button>
<button <button
class="bp3-button bp3-disabled bp3-intent-primary" class="bp3-button bp3-intent-primary"
disabled=""
tabindex="-1"
type="button" type="button"
> >
<span <span

View File

@ -23,8 +23,8 @@
width: 600px; width: 600px;
} }
.ace_editor { .auto-form {
margin: 0px 20px 10px; margin: 5px 20px 10px;
} }
.lookup-label { .lookup-label {

View File

@ -19,7 +19,7 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { LookupEditDialog } from './lookup-edit-dialog'; import { isDisabled, LookupEditDialog } from './lookup-edit-dialog';
describe('lookup edit dialog', () => { describe('lookup edit dialog', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
@ -31,7 +31,7 @@ describe('lookup edit dialog', () => {
lookupName={'test'} lookupName={'test'}
lookupTier={'test'} lookupTier={'test'}
lookupVersion={'test'} lookupVersion={'test'}
lookupSpec={'test'} lookupSpec={{ type: 'map', map: {} }}
isEdit={false} isEdit={false}
allLookupTiers={['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']} allLookupTiers={['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']}
/> />
@ -41,3 +41,433 @@ describe('lookup edit dialog', () => {
expect(document.body.lastChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });
describe('Type Map Should be disabled', () => {
it('Missing LookupName', () => {
expect(isDisabled(undefined, 'v1', '__default', { type: '' })).toBe(true);
});
it('Empty version', () => {
expect(isDisabled('lookup', '', '__default', { type: '' })).toBe(true);
});
it('Missing version', () => {
expect(isDisabled('lookup', undefined, '__default', { type: '' })).toBe(true);
});
it('Empty tier', () => {
expect(isDisabled('lookup', 'v1', '', { type: '' })).toBe(true);
});
it('Missing tier', () => {
expect(isDisabled('lookup', 'v1', undefined, { type: '' })).toBe(true);
});
it('Missing spec', () => {
expect(isDisabled('lookup', 'v1', '__default', {})).toBe(true);
});
it('Type undefined', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: undefined })).toBe(true);
});
it('Lookup of type map with no map', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: 'map' })).toBe(true);
});
it('Lookup of type cachedNamespace with no extractionNamespace', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: 'cachedNamespace' })).toBe(true);
});
it('Lookup of type cachedNamespace with extractionNamespace type uri, format csv, no namespaceParseSpec', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
it('Lookup of type cachedNamespace with extractionNamespace type uri, format csv, no columns and skipHeaderRows', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'csv',
},
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
it('Lookup of type cachedNamespace with extractionNamespace type uri, format tsv, no columns', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'tsv',
skipHeaderRows: 0,
},
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
it('Lookup of type cachedNamespace with extractionNamespace type customJson, format tsv, no keyFieldName', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'customJson',
valueFieldName: 'value',
},
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
it('Lookup of type cachedNamespace with extractionNamespace type customJson, format customJson, no valueFieldName', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'customJson',
keyFieldName: 'key',
},
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
});
describe('Type cachedNamespace should be disabled', () => {
it('No extractionNamespace', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: 'cachedNamespace' })).toBe(true);
});
describe('ExtractionNamespace type URI', () => {
it('Format csv, no namespaceParseSpec', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
it('Format csv, no columns and skipHeaderRows', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'csv',
},
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
it('Format tsv, no columns', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'tsv',
skipHeaderRows: 0,
},
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
it('Format tsv, no keyFieldName', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'customJson',
valueFieldName: 'value',
},
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
it('Format customJson, no valueFieldName', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'customJson',
keyFieldName: 'key',
},
pollPeriod: 'PT5M',
},
}),
).toBe(true);
});
});
describe('ExtractionNamespace type JDBC', () => {
it('No namespace', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'jdbc',
namespace: undefined,
connectorConfig: {
createTables: true,
connectURI: 'jdbc:mysql://localhost:3306/druid',
user: 'druid',
password: 'diurd',
},
table: 'some_lookup_table',
keyColumn: 'the_old_dim_value',
valueColumn: 'the_new_dim_value',
tsColumn: 'timestamp_column',
pollPeriod: 600000,
},
}),
).toBe(true);
});
it('No connectorConfig', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'jdbc',
namespace: 'some_lookup',
connectorConfig: undefined,
table: 'some_lookup_table',
keyColumn: 'the_old_dim_value',
valueColumn: 'the_new_dim_value',
tsColumn: 'timestamp_column',
pollPeriod: 600000,
},
}),
).toBe(true);
});
it('No table', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'jdbc',
namespace: 'some_lookup',
connectorConfig: {
createTables: true,
connectURI: 'jdbc:mysql://localhost:3306/druid',
user: 'druid',
password: 'diurd',
},
table: undefined,
keyColumn: 'the_old_dim_value',
valueColumn: 'the_new_dim_value',
tsColumn: 'timestamp_column',
pollPeriod: 600000,
},
}),
).toBe(true);
});
it('No keyColumn', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'jdbc',
namespace: 'some_lookup',
connectorConfig: {
createTables: true,
connectURI: 'jdbc:mysql://localhost:3306/druid',
user: 'druid',
password: 'diurd',
},
table: 'some_lookup_table',
keyColumn: undefined,
valueColumn: 'the_new_dim_value',
tsColumn: 'timestamp_column',
pollPeriod: 600000,
},
}),
).toBe(true);
});
it('No keyColumn', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'jdbc',
namespace: 'some_lookup',
connectorConfig: {
createTables: true,
connectURI: 'jdbc:mysql://localhost:3306/druid',
user: 'druid',
password: 'diurd',
},
table: 'some_lookup_table',
keyColumn: 'the_old_dim_value',
valueColumn: undefined,
tsColumn: 'timestamp_column',
pollPeriod: 600000,
},
}),
).toBe(true);
});
});
});
describe('Type Map Should be enabled', () => {
it('Has type and has Map', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: 'map', map: { a: 'b' } })).toBe(false);
});
});
describe('Type cachedNamespace Should be enabled', () => {
describe('ExtractionNamespace type URI', () => {
it('Format csv with columns', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'csv',
columns: ['key', 'value'],
},
},
}),
).toBe(false);
});
it('Format csv with skipHeaderRows', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'csv',
skipHeaderRows: 1,
},
},
}),
).toBe(false);
});
it('Format tsv, only columns', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'tsv',
columns: ['key', 'value'],
},
},
}),
).toBe(false);
});
it('Format tsv, keyFieldName and valueFieldName', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'uri',
uriPrefix: 's3://bucket/some/key/prefix/',
fileRegex: 'renames-[0-9]*\\.gz',
namespaceParseSpec: {
format: 'customJson',
valueFieldName: 'value',
keyFieldName: 'value',
},
},
}),
).toBe(false);
});
});
describe('ExtractionNamespace type JDBC', () => {
it('No namespace', () => {
expect(
isDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace',
extractionNamespace: {
type: 'jdbc',
namespace: 'lookup',
connectorConfig: {
createTables: true,
connectURI: 'jdbc:mysql://localhost:3306/druid',
user: 'druid',
password: 'diurd',
},
table: 'some_lookup_table',
keyColumn: 'the_old_dim_value',
valueColumn: 'the_new_dim_value',
},
}),
).toBe(false);
});
});
});

View File

@ -26,24 +26,120 @@ import {
Intent, Intent,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import React from 'react'; import React from 'react';
import AceEditor from 'react-ace';
import { validJson } from '../../utils'; import { AutoForm, Field } from '../../components';
import './lookup-edit-dialog.scss'; import './lookup-edit-dialog.scss';
export interface ExtractionNamespaceSpec {
type?: string;
uri?: string;
uriPrefix?: string;
fileRegex?: string;
namespaceParseSpec?: NamespaceParseSpec;
namespace?: string;
connectorConfig?: {
createTables: boolean;
connectURI: string;
user: string;
password: string;
};
table?: string;
keyColumn?: string;
valueColumn?: string;
filter?: any;
tsColumn?: string;
pollPeriod?: number | string;
}
export interface NamespaceParseSpec {
format: string;
columns?: string[];
keyColumn?: string;
valueColumn?: string;
hasHeaderRow?: boolean;
skipHeaderRows?: number;
keyFieldName?: string;
valueFieldName?: string;
delimiter?: string;
listDelimiter?: string;
}
export interface LookupSpec {
type?: string;
map?: {};
extractionNamespace?: ExtractionNamespaceSpec;
firstCacheTimeout?: number;
injective?: boolean;
}
export interface LookupEditDialogProps { export interface LookupEditDialogProps {
onClose: () => void; onClose: () => void;
onSubmit: () => void; onSubmit: (updateLookupVersion: boolean) => void;
onChange: (field: string, value: string) => void; onChange: (field: string, value: string | LookupSpec) => void;
lookupName: string; lookupName: string;
lookupTier: string; lookupTier: string;
lookupVersion: string; lookupVersion: string;
lookupSpec: string; lookupSpec: LookupSpec;
isEdit: boolean; isEdit: boolean;
allLookupTiers: string[]; allLookupTiers: string[];
} }
export function isDisabled(
lookupName?: string,
lookupVersion?: string,
lookupTier?: string,
lookupSpec?: LookupSpec,
) {
let disableSubmit =
!lookupName ||
!lookupVersion ||
!lookupTier ||
!lookupSpec ||
!lookupName ||
lookupName === '' ||
lookupVersion === '' ||
lookupTier === '' ||
lookupSpec.type === '' ||
lookupSpec.type === undefined ||
(lookupSpec.type === 'map' && lookupSpec.map === undefined) ||
(lookupSpec.type === 'cachedNamespace' && lookupSpec.extractionNamespace === undefined);
if (
!disableSubmit &&
lookupSpec &&
lookupSpec.type === 'cachedNamespace' &&
lookupSpec.extractionNamespace
) {
switch (lookupSpec.extractionNamespace.type) {
case 'uri':
const namespaceParseSpec = lookupSpec.extractionNamespace.namespaceParseSpec;
disableSubmit = !namespaceParseSpec;
if (!namespaceParseSpec) break;
switch (namespaceParseSpec.format) {
case 'csv':
disableSubmit = !namespaceParseSpec.columns && !namespaceParseSpec.skipHeaderRows;
break;
case 'tsv':
disableSubmit = !namespaceParseSpec.columns;
break;
case 'customJson':
disableSubmit = !namespaceParseSpec.keyFieldName || !namespaceParseSpec.valueFieldName;
break;
}
break;
case 'jdbc':
const extractionNamespace = lookupSpec.extractionNamespace;
disableSubmit =
!extractionNamespace.namespace ||
!extractionNamespace.connectorConfig ||
!extractionNamespace.table ||
!extractionNamespace.keyColumn ||
!extractionNamespace.valueColumn;
break;
}
}
return disableSubmit;
}
export const LookupEditDialog = React.memo(function LookupEditDialog(props: LookupEditDialogProps) { export const LookupEditDialog = React.memo(function LookupEditDialog(props: LookupEditDialogProps) {
const { const {
onClose, onClose,
@ -57,6 +153,8 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
allLookupTiers, allLookupTiers,
} = props; } = props;
let updateVersionOnSubmit = true;
function addISOVersion() { function addISOVersion() {
const currentDate = new Date(); const currentDate = new Date();
const ISOString = currentDate.toISOString(); const ISOString = currentDate.toISOString();
@ -66,17 +164,20 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
function renderTierInput() { function renderTierInput() {
if (isEdit) { if (isEdit) {
return ( return (
<FormGroup className="lookup-label" label="Tier: "> <FormGroup className="lookup-label" label="Tier">
<InputGroup <InputGroup
value={lookupTier} value={lookupTier}
onChange={(e: any) => onChange('lookupEditTier', e.target.value)} onChange={(e: any) => {
updateVersionOnSubmit = false;
onChange('lookupEditTier', e.target.value);
}}
disabled disabled
/> />
</FormGroup> </FormGroup>
); );
} else { } else {
return ( return (
<FormGroup className="lookup-label" label="Tier:"> <FormGroup className="lookup-label" label="Tier">
<HTMLSelect <HTMLSelect
disabled={isEdit} disabled={isEdit}
value={lookupTier} value={lookupTier}
@ -93,8 +194,383 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
} }
} }
const disableSubmit = const fields = [
lookupName === '' || lookupVersion === '' || lookupTier === '' || !validJson(lookupSpec); {
name: 'type',
type: 'string',
suggestions: ['map', 'cachedNamespace'],
adjustment: (model: LookupSpec) => {
if (model.type === 'map' && model.extractionNamespace && model.extractionNamespace.type) {
return model;
}
model.extractionNamespace = { type: 'uri', namespaceParseSpec: { format: 'csv' } };
return model;
},
},
{
name: 'map',
type: 'json',
defined: (model: LookupSpec) => {
return model.type === 'map';
},
},
{
name: 'extractionNamespace.type',
type: 'string',
label: 'Globally cached lookup type',
placeholder: 'uri',
suggestions: ['uri', 'jdbc'],
defined: (model: LookupSpec) => model.type === 'cachedNamespace',
},
{
name: 'extractionNamespace.uriPrefix',
type: 'string',
label: 'URI prefix',
info:
'A URI which specifies a directory (or other searchable resource) in which to search for files',
placeholder: 's3://bucket/some/key/prefix/',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri',
},
{
name: 'extractionNamespace.fileRegex',
type: 'string',
label: 'File regex',
placeholder: '(optional)',
info:
'Optional regex for matching the file name under uriPrefix. Only used if uriPrefix is used',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri',
},
{
name: 'extractionNamespace.namespaceParseSpec.format',
type: 'string',
label: 'Format',
defaultValue: 'csv',
suggestions: ['csv', 'tsv', 'customJson', 'simpleJson'],
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri',
},
{
name: 'extractionNamespace.namespaceParseSpec.columns',
type: 'string-array',
label: 'Columns',
placeholder: `["key", "value"]`,
info: 'The list of columns in the csv file',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
(model.extractionNamespace.namespaceParseSpec.format === 'csv' ||
model.extractionNamespace.namespaceParseSpec.format === 'tsv'),
},
{
name: 'extractionNamespace.namespaceParseSpec.keyColumn',
type: 'string',
label: 'Key column',
placeholder: 'Key',
info: 'The name of the column containing the key',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
(model.extractionNamespace.namespaceParseSpec.format === 'csv' ||
model.extractionNamespace.namespaceParseSpec.format === 'tsv'),
},
{
name: 'extractionNamespace.namespaceParseSpec.valueColumn',
type: 'string',
label: 'Value column',
placeholder: 'Value',
info: 'The name of the column containing the value',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
(model.extractionNamespace.namespaceParseSpec.format === 'csv' ||
model.extractionNamespace.namespaceParseSpec.format === 'tsv'),
},
{
name: 'extractionNamespace.namespaceParseSpec.hasHeaderRow',
type: 'boolean',
label: 'Has header row',
defaultValue: false,
info: `A flag to indicate that column information can be extracted from the input files' header row`,
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
(model.extractionNamespace.namespaceParseSpec.format === 'csv' ||
model.extractionNamespace.namespaceParseSpec.format === 'tsv'),
},
{
name: 'extractionNamespace.namespaceParseSpec.skipHeaderRows',
type: 'number',
label: 'Skip header rows',
placeholder: '(optional)',
info: `Number of header rows to be skipped. The default number of header rows to be skipped is 0.`,
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
(model.extractionNamespace.namespaceParseSpec.format === 'csv' ||
model.extractionNamespace.namespaceParseSpec.format === 'tsv'),
},
{
name: 'extractionNamespace.namespaceParseSpec.delimiter',
type: 'string',
label: 'Delimiter',
placeholder: `(optional)`,
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
model.extractionNamespace.namespaceParseSpec.format === 'tsv',
},
{
name: 'extractionNamespace.namespaceParseSpec.listDelimiter',
type: 'string',
label: 'List delimiter',
placeholder: `(optional)`,
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
model.extractionNamespace.namespaceParseSpec.format === 'tsv',
},
{
name: 'extractionNamespace.namespaceParseSpec.keyFieldName',
type: 'string',
label: 'Key field name',
placeholder: `key`,
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
model.extractionNamespace.namespaceParseSpec.format === 'customJson',
},
{
name: 'extractionNamespace.namespaceParseSpec.valueFieldName',
type: 'string',
label: 'Value field name',
placeholder: `value`,
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri' &&
model.extractionNamespace.namespaceParseSpec &&
model.extractionNamespace.namespaceParseSpec.format === 'customJson',
},
{
name: 'extractionNamespace.namespace',
type: 'string',
label: 'Namespace',
placeholder: 'some_lookup',
info: (
<>
<p>The namespace value in the SQL query:</p>
<p>
SELECT keyColumn, valueColumn, tsColumn? FROM <strong>namespace</strong>.table WHERE
filter
</p>
</>
),
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.connectorConfig.createTables',
type: 'boolean',
label: 'CreateTables',
info: 'Defines the connectURI value on the The connector config to used',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.connectorConfig.connectURI',
type: 'string',
label: 'Connect URI',
info: 'Defines the connectURI value on the The connector config to used',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.connectorConfig.user',
type: 'string',
label: 'User',
info: 'Defines the user to be used by the connector config',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.connectorConfig.password',
type: 'string',
label: 'Password',
info: 'Defines the password to be used by the connector config',
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.table',
type: 'string',
label: 'Table',
placeholder: 'some_lookup_table',
info: (
<>
<p>
The table which contains the key value pairs. This will become the table value in the
SQL query:
</p>
<p>
SELECT keyColumn, valueColumn, tsColumn? FROM namespace.<strong>table</strong> WHERE
filter
</p>
</>
),
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.keyColumn',
type: 'string',
label: 'Key column',
placeholder: 'my_key_value',
info: (
<>
<p>
The column in the table which contains the keys. This will become the keyColumn value in
the SQL query:
</p>
<p>
SELECT <strong>keyColumn</strong>, valueColumn, tsColumn? FROM namespace.table WHERE
filter
</p>
</>
),
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.valueColumn',
type: 'string',
label: 'Value column',
placeholder: 'my_column_value',
info: (
<>
<p>
The column in table which contains the values. This will become the valueColumn value in
the SQL query:
</p>
<p>
SELECT keyColumn, <strong>valueColumn</strong>, tsColumn? FROM namespace.table WHERE
filter
</p>
</>
),
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.filter',
type: 'string',
label: 'Filter',
placeholder: '(optional)',
info: (
<>
<p>
The filter to be used when selecting lookups, this is used to create a where clause on
lookup population. This will become the expression filter in the SQL query:
</p>
<p>
SELECT keyColumn, valueColumn, tsColumn? FROM namespace.table WHERE{' '}
<strong>filter</strong>
</p>
</>
),
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.tsColumn',
type: 'string',
label: 'TsColumn',
placeholder: '(optional)',
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>
</>
),
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'jdbc',
},
{
name: 'extractionNamespace.pollPeriod',
type: 'string',
label: 'Poll period',
placeholder: '(optional)',
info: `Period between polling for updates`,
defined: (model: LookupSpec) =>
model.type === 'cachedNamespace' &&
!!model.extractionNamespace &&
model.extractionNamespace.type === 'uri',
},
{
name: 'firstCacheTimeout',
type: 'number',
label: 'First cache timeout',
placeholder: '(optional)',
info: `How long to wait (in ms) for the first run of the cache to populate. 0 indicates to not wait`,
defined: (model: LookupSpec) => model.type === 'cachedNamespace',
},
{
name: 'injective',
type: 'boolean',
defaultValue: false,
info: `If the underlying map is injective (keys and values are unique) then optimizations can occur internally by setting this to true`,
defined: (model: LookupSpec) => model.type === 'cachedNamespace',
},
];
return ( return (
<Dialog <Dialog
@ -103,7 +579,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
onClose={onClose} onClose={onClose}
title={isEdit ? 'Edit lookup' : 'Add lookup'} title={isEdit ? 'Edit lookup' : 'Add lookup'}
> >
<FormGroup className="lookup-label" label="Name: "> <FormGroup className="lookup-label" label="Name">
<InputGroup <InputGroup
value={lookupName} value={lookupName}
onChange={(e: any) => onChange('lookupEditName', e.target.value)} onChange={(e: any) => onChange('lookupEditName', e.target.value)}
@ -111,10 +587,8 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
placeholder="Enter the lookup name" placeholder="Enter the lookup name"
/> />
</FormGroup> </FormGroup>
{renderTierInput()} {renderTierInput()}
<FormGroup className="lookup-label" label="Version">
<FormGroup className="lookup-label" label="Version:">
<InputGroup <InputGroup
value={lookupVersion} value={lookupVersion}
onChange={(e: any) => onChange('lookupEditVersion', e.target.value)} onChange={(e: any) => onChange('lookupEditVersion', e.target.value)}
@ -124,35 +598,23 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
} }
/> />
</FormGroup> </FormGroup>
<AutoForm
<FormGroup className="lookup-label" label="Spec:" /> fields={fields as Field<LookupSpec>[]}
model={lookupSpec}
<AceEditor onChange={m => {
className="lookup-edit-dialog-textarea" onChange('lookupEditSpec', m);
mode="hjson"
theme="solarized_dark"
onChange={(e: any) => onChange('lookupEditSpec', e)}
fontSize={12}
height="40vh"
width="auto"
showPrintMargin={false}
showGutter={false}
value={lookupSpec}
editorProps={{ $blockScrolling: Infinity }}
setOptions={{
tabSize: 2,
}} }}
style={{}}
/> />
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} /> <Button text="Close" onClick={onClose} />
<Button <Button
text="Submit" text="Submit"
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
onClick={() => onSubmit()} onClick={() => {
disabled={disableSubmit} onSubmit(updateVersionOnSubmit && isEdit);
}}
disabled={isDisabled(lookupName, lookupVersion, lookupTier, lookupSpec)}
/> />
</div> </div>
</div> </div>

View File

@ -32,6 +32,7 @@ import {
ViewControlBar, ViewControlBar,
} from '../../components'; } from '../../components';
import { AsyncActionDialog, LookupEditDialog } from '../../dialogs/'; import { AsyncActionDialog, LookupEditDialog } from '../../dialogs/';
import { LookupSpec } from '../../dialogs/lookup-edit-dialog/lookup-edit-dialog';
import { LookupTableActionDialog } from '../../dialogs/lookup-table-action-dialog/lookup-table-action-dialog'; import { LookupTableActionDialog } from '../../dialogs/lookup-table-action-dialog/lookup-table-action-dialog';
import { AppToaster } from '../../singletons/toaster'; import { AppToaster } from '../../singletons/toaster';
import { getDruidErrorMessage, LocalStorageKeys, QueryManager } from '../../utils'; import { getDruidErrorMessage, LocalStorageKeys, QueryManager } from '../../utils';
@ -55,7 +56,7 @@ export interface LookupsViewState {
lookupEditName: string; lookupEditName: string;
lookupEditTier: string; lookupEditTier: string;
lookupEditVersion: string; lookupEditVersion: string;
lookupEditSpec: string; lookupEditSpec: LookupSpec;
isEdit: boolean; isEdit: boolean;
allLookupTiers: string[]; allLookupTiers: string[];
@ -81,7 +82,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
lookupEditTier: '', lookupEditTier: '',
lookupEditName: '', lookupEditName: '',
lookupEditVersion: '', lookupEditVersion: '',
lookupEditSpec: '', lookupEditSpec: { type: '' },
isEdit: false, isEdit: false,
allLookupTiers: [], allLookupTiers: [],
actions: [], actions: [],
@ -162,7 +163,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
lookupEditName: '', lookupEditName: '',
lookupEditTier: prevState.allLookupTiers[0], lookupEditTier: prevState.allLookupTiers[0],
lookupEditDialogOpen: true, lookupEditDialogOpen: true,
lookupEditSpec: '', lookupEditSpec: { type: '' },
lookupEditVersion: new Date().toISOString(), lookupEditVersion: new Date().toISOString(),
isEdit: false, isEdit: false,
})); }));
@ -171,20 +172,20 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
lookupEditName: id, lookupEditName: id,
lookupEditTier: tier, lookupEditTier: tier,
lookupEditDialogOpen: true, lookupEditDialogOpen: true,
lookupEditSpec: JSON.stringify(target.spec, null, 2), lookupEditSpec: target.spec,
lookupEditVersion: target.version, lookupEditVersion: target.version,
isEdit: true, isEdit: true,
}); });
} }
} }
private handleChangeLookup = (field: string, value: string) => { private handleChangeLookup = (field: string, value: string | LookupSpec) => {
this.setState({ this.setState({
[field]: value, [field]: value,
} as any); } as any);
}; };
private async submitLookupEdit() { private async submitLookupEdit(updatelookupEditVersion: boolean) {
const { const {
lookupEditTier, lookupEditTier,
lookupEditName, lookupEditName,
@ -192,20 +193,21 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
lookupEditVersion, lookupEditVersion,
isEdit, isEdit,
} = this.state; } = this.state;
const version = updatelookupEditVersion ? new Date().toISOString() : lookupEditVersion;
let endpoint = '/druid/coordinator/v1/lookups/config'; let endpoint = '/druid/coordinator/v1/lookups/config';
const specJson: any = JSON.parse(lookupEditSpec); const specJson: any = lookupEditSpec;
let dataJson: any; let dataJson: any;
if (isEdit) { if (isEdit) {
endpoint = `${endpoint}/${lookupEditTier}/${lookupEditName}`; endpoint = `${endpoint}/${lookupEditTier}/${lookupEditName}`;
dataJson = { dataJson = {
version: lookupEditVersion, version: version,
lookupExtractorFactory: specJson, lookupExtractorFactory: specJson,
}; };
} else { } else {
dataJson = { dataJson = {
[lookupEditTier]: { [lookupEditTier]: {
[lookupEditName]: { [lookupEditName]: {
version: lookupEditVersion, version: version,
lookupExtractorFactory: specJson, lookupExtractorFactory: specJson,
}, },
}, },
@ -374,7 +376,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
return ( return (
<LookupEditDialog <LookupEditDialog
onClose={() => this.setState({ lookupEditDialogOpen: false })} onClose={() => this.setState({ lookupEditDialogOpen: false })}
onSubmit={() => this.submitLookupEdit()} onSubmit={updateLookupVersion => this.submitLookupEdit(updateLookupVersion)}
onChange={this.handleChangeLookup} onChange={this.handleChangeLookup}
lookupSpec={lookupEditSpec} lookupSpec={lookupEditSpec}
lookupName={lookupEditName} lookupName={lookupEditName}