blob: 6c7061ef63c076ac0aa6fa38568f36acd195dbbf [file] [log] [blame]
/*
* 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 {
Button,
Classes,
Dialog,
FormGroup,
HTMLSelect,
InputGroup,
Intent,
} from '@blueprintjs/core';
import React, { useState } from 'react';
import { AutoForm, Field, JsonInput } from '../../components';
import {
FormJsonSelector,
FormJsonTabs,
} from '../../components/form-json-selector/form-json-selector';
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 {
onClose: () => void;
onSubmit: (updateLookupVersion: boolean) => void;
onChange: (field: 'name' | 'tier' | 'version' | 'spec', value: string | LookupSpec) => void;
lookupName: string;
lookupTier: string;
lookupVersion: string;
lookupSpec: LookupSpec;
isEdit: boolean;
allLookupTiers: string[];
}
export function isLookupSubmitDisabled(
lookupName: string | undefined,
lookupVersion: string | undefined,
lookupTier: string | undefined,
lookupSpec: LookupSpec | undefined,
) {
let disableSubmit =
!lookupName ||
!lookupVersion ||
!lookupTier ||
!lookupSpec ||
!lookupSpec.type ||
(lookupSpec.type === 'map' && !lookupSpec.map) ||
(lookupSpec.type === 'cachedNamespace' && !lookupSpec.extractionNamespace);
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;
}
const LOOKUP_FIELDS: Field<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',
height: '60vh',
defined: (model: LookupSpec) => 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) =>
Boolean(
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) =>
Boolean(
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) =>
Boolean(
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) =>
Boolean(
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) =>
Boolean(
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) =>
Boolean(
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) =>
Boolean(
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) =>
Boolean(
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) =>
Boolean(
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) =>
Boolean(
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',
},
];
export const LookupEditDialog = React.memo(function LookupEditDialog(props: LookupEditDialogProps) {
const {
onClose,
onSubmit,
lookupSpec,
lookupTier,
lookupName,
lookupVersion,
onChange,
isEdit,
allLookupTiers,
} = props;
const [currentTab, setCurrentTab] = useState<FormJsonTabs>('form');
const [updateVersionOnSubmit, setUpdateVersionOnSubmit] = useState(true);
return (
<Dialog
className="lookup-edit-dialog"
isOpen
onClose={onClose}
title={isEdit ? 'Edit lookup' : 'Add lookup'}
>
<div className="content">
<FormGroup label="Name">
<InputGroup
value={lookupName}
onChange={(e: any) => onChange('name', e.target.value)}
disabled={isEdit}
placeholder="Enter the lookup name"
/>
</FormGroup>
<FormGroup label="Tier">
{isEdit ? (
<InputGroup
value={lookupTier}
onChange={(e: any) => onChange('tier', e.target.value)}
disabled
/>
) : (
<HTMLSelect value={lookupTier} onChange={(e: any) => onChange('tier', e.target.value)}>
{allLookupTiers.map(tier => (
<option key={tier} value={tier}>
{tier}
</option>
))}
</HTMLSelect>
)}
</FormGroup>
<FormGroup label="Version">
<InputGroup
value={lookupVersion}
onChange={(e: any) => {
setUpdateVersionOnSubmit(false);
onChange('version', e.target.value);
}}
placeholder="Enter the lookup version"
rightElement={
<Button
minimal
text="Use ISO as version"
onClick={() => onChange('version', new Date().toISOString())}
/>
}
/>
</FormGroup>
<FormJsonSelector tab={currentTab} onChange={setCurrentTab} />
{currentTab === 'form' ? (
<AutoForm
fields={LOOKUP_FIELDS}
model={lookupSpec}
onChange={m => {
onChange('spec', m);
}}
/>
) : (
<JsonInput
value={lookupSpec}
onChange={m => {
onChange('spec', m);
}}
height="80vh"
/>
)}
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} />
<Button
text="Submit"
intent={Intent.PRIMARY}
onClick={() => {
onSubmit(updateVersionOnSubmit && isEdit);
}}
disabled={isLookupSubmitDisabled(lookupName, lookupVersion, lookupTier, lookupSpec)}
/>
</div>
</div>
</Dialog>
);
});