Web console: fix lookup edit dialog, allow column renaming (#10406)

* column rename

* update licenses file

* remove empty file

* update license file

* move comment
This commit is contained in:
Vadim Ogievetsky 2020-09-20 14:10:05 -07:00 committed by GitHub
parent ae247b6e63
commit 6c5c86d800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 651 additions and 469 deletions

View File

@ -4721,7 +4721,7 @@ license_category: binary
module: web-console module: web-console
license_name: Apache License version 2.0 license_name: Apache License version 2.0
copyright: Imply Data copyright: Imply Data
version: 0.9.11 version: 0.9.15
--- ---

View File

@ -4243,9 +4243,9 @@
} }
}, },
"druid-query-toolkit": { "druid-query-toolkit": {
"version": "0.9.11", "version": "0.9.15",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.9.11.tgz", "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.9.15.tgz",
"integrity": "sha512-pg0Ux/y0IM2TxYEfY2cK361w+c5Efw40KVrkIK8s8uVDYF3BLVN+XWQO/ykKB73hOUHLq1ctLOxRUyKphnbyvQ==", "integrity": "sha512-HiJVqa6X6z/LeEU3GVxbkaP/X8JCn/9n8txoG1tSgk6edc1BZXj7F7hZpaNlAThVcZcFOYeE1gFOHzMMjK4U0A==",
"requires": { "requires": {
"tslib": "^1.10.0" "tslib": "^1.10.0"
} }

View File

@ -68,7 +68,7 @@
"d3-axis": "^1.0.12", "d3-axis": "^1.0.12",
"d3-scale": "^3.2.0", "d3-scale": "^3.2.0",
"d3-selection": "^1.4.0", "d3-selection": "^1.4.0",
"druid-query-toolkit": "^0.9.11", "druid-query-toolkit": "^0.9.15",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"has-own-prop": "^2.0.0", "has-own-prop": "^2.0.0",
"hjson": "^3.2.1", "hjson": "^3.2.1",

View File

@ -19,7 +19,7 @@
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import { isDisabled, LookupEditDialog } from './lookup-edit-dialog'; import { isLookupSubmitDisabled, LookupEditDialog } from './lookup-edit-dialog';
describe('lookup edit dialog', () => { describe('lookup edit dialog', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
@ -43,44 +43,46 @@ describe('lookup edit dialog', () => {
describe('Type Map Should be disabled', () => { describe('Type Map Should be disabled', () => {
it('Missing LookupName', () => { it('Missing LookupName', () => {
expect(isDisabled(undefined, 'v1', '__default', { type: '' })).toBe(true); expect(isLookupSubmitDisabled(undefined, 'v1', '__default', { type: '' })).toBe(true);
}); });
it('Empty version', () => { it('Empty version', () => {
expect(isDisabled('lookup', '', '__default', { type: '' })).toBe(true); expect(isLookupSubmitDisabled('lookup', '', '__default', { type: '' })).toBe(true);
}); });
it('Missing version', () => { it('Missing version', () => {
expect(isDisabled('lookup', undefined, '__default', { type: '' })).toBe(true); expect(isLookupSubmitDisabled('lookup', undefined, '__default', { type: '' })).toBe(true);
}); });
it('Empty tier', () => { it('Empty tier', () => {
expect(isDisabled('lookup', 'v1', '', { type: '' })).toBe(true); expect(isLookupSubmitDisabled('lookup', 'v1', '', { type: '' })).toBe(true);
}); });
it('Missing tier', () => { it('Missing tier', () => {
expect(isDisabled('lookup', 'v1', undefined, { type: '' })).toBe(true); expect(isLookupSubmitDisabled('lookup', 'v1', undefined, { type: '' })).toBe(true);
}); });
it('Missing spec', () => { it('Missing spec', () => {
expect(isDisabled('lookup', 'v1', '__default', {})).toBe(true); expect(isLookupSubmitDisabled('lookup', 'v1', '__default', {})).toBe(true);
}); });
it('Type undefined', () => { it('Type undefined', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: undefined })).toBe(true); expect(isLookupSubmitDisabled('lookup', 'v1', '__default', { type: undefined })).toBe(true);
}); });
it('Lookup of type map with no map', () => { it('Lookup of type map with no map', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: 'map' })).toBe(true); expect(isLookupSubmitDisabled('lookup', 'v1', '__default', { type: 'map' })).toBe(true);
}); });
it('Lookup of type cachedNamespace with no extractionNamespace', () => { it('Lookup of type cachedNamespace with no extractionNamespace', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: 'cachedNamespace' })).toBe(true); expect(isLookupSubmitDisabled('lookup', 'v1', '__default', { type: 'cachedNamespace' })).toBe(
true,
);
}); });
it('Lookup of type cachedNamespace with extractionNamespace type uri, format csv, no namespaceParseSpec', () => { it('Lookup of type cachedNamespace with extractionNamespace type uri, format csv, no namespaceParseSpec', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -94,7 +96,7 @@ describe('Type Map Should be disabled', () => {
it('Lookup of type cachedNamespace with extractionNamespace type uri, format csv, no columns and skipHeaderRows', () => { it('Lookup of type cachedNamespace with extractionNamespace type uri, format csv, no columns and skipHeaderRows', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -111,7 +113,7 @@ describe('Type Map Should be disabled', () => {
it('Lookup of type cachedNamespace with extractionNamespace type uri, format tsv, no columns', () => { it('Lookup of type cachedNamespace with extractionNamespace type uri, format tsv, no columns', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -129,7 +131,7 @@ describe('Type Map Should be disabled', () => {
it('Lookup of type cachedNamespace with extractionNamespace type customJson, format tsv, no keyFieldName', () => { it('Lookup of type cachedNamespace with extractionNamespace type customJson, format tsv, no keyFieldName', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -147,7 +149,7 @@ describe('Type Map Should be disabled', () => {
it('Lookup of type cachedNamespace with extractionNamespace type customJson, format customJson, no valueFieldName', () => { it('Lookup of type cachedNamespace with extractionNamespace type customJson, format customJson, no valueFieldName', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -166,13 +168,15 @@ describe('Type Map Should be disabled', () => {
describe('Type cachedNamespace should be disabled', () => { describe('Type cachedNamespace should be disabled', () => {
it('No extractionNamespace', () => { it('No extractionNamespace', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: 'cachedNamespace' })).toBe(true); expect(isLookupSubmitDisabled('lookup', 'v1', '__default', { type: 'cachedNamespace' })).toBe(
true,
);
}); });
describe('ExtractionNamespace type URI', () => { describe('ExtractionNamespace type URI', () => {
it('Format csv, no namespaceParseSpec', () => { it('Format csv, no namespaceParseSpec', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -186,7 +190,7 @@ describe('Type cachedNamespace should be disabled', () => {
it('Format csv, no columns and skipHeaderRows', () => { it('Format csv, no columns and skipHeaderRows', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -203,7 +207,7 @@ describe('Type cachedNamespace should be disabled', () => {
it('Format tsv, no columns', () => { it('Format tsv, no columns', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -221,7 +225,7 @@ describe('Type cachedNamespace should be disabled', () => {
it('Format tsv, no keyFieldName', () => { it('Format tsv, no keyFieldName', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -239,7 +243,7 @@ describe('Type cachedNamespace should be disabled', () => {
it('Format customJson, no valueFieldName', () => { it('Format customJson, no valueFieldName', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -259,7 +263,7 @@ describe('Type cachedNamespace should be disabled', () => {
describe('ExtractionNamespace type JDBC', () => { describe('ExtractionNamespace type JDBC', () => {
it('No namespace', () => { it('No namespace', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'jdbc', type: 'jdbc',
@ -282,7 +286,7 @@ describe('Type cachedNamespace should be disabled', () => {
it('No connectorConfig', () => { it('No connectorConfig', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'jdbc', type: 'jdbc',
@ -300,7 +304,7 @@ describe('Type cachedNamespace should be disabled', () => {
it('No table', () => { it('No table', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'jdbc', type: 'jdbc',
@ -323,7 +327,7 @@ describe('Type cachedNamespace should be disabled', () => {
it('No keyColumn', () => { it('No keyColumn', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'jdbc', type: 'jdbc',
@ -346,7 +350,7 @@ describe('Type cachedNamespace should be disabled', () => {
it('No keyColumn', () => { it('No keyColumn', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'jdbc', type: 'jdbc',
@ -371,7 +375,9 @@ describe('Type cachedNamespace should be disabled', () => {
describe('Type Map Should be enabled', () => { describe('Type Map Should be enabled', () => {
it('Has type and has Map', () => { it('Has type and has Map', () => {
expect(isDisabled('lookup', 'v1', '__default', { type: 'map', map: { a: 'b' } })).toBe(false); expect(
isLookupSubmitDisabled('lookup', 'v1', '__default', { type: 'map', map: { a: 'b' } }),
).toBe(false);
}); });
}); });
@ -379,7 +385,7 @@ describe('Type cachedNamespace Should be enabled', () => {
describe('ExtractionNamespace type URI', () => { describe('ExtractionNamespace type URI', () => {
it('Format csv with columns', () => { it('Format csv with columns', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -396,7 +402,7 @@ describe('Type cachedNamespace Should be enabled', () => {
it('Format csv with skipHeaderRows', () => { it('Format csv with skipHeaderRows', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -413,7 +419,7 @@ describe('Type cachedNamespace Should be enabled', () => {
it('Format tsv, only columns', () => { it('Format tsv, only columns', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -430,7 +436,7 @@ describe('Type cachedNamespace Should be enabled', () => {
it('Format tsv, keyFieldName and valueFieldName', () => { it('Format tsv, keyFieldName and valueFieldName', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'uri', type: 'uri',
@ -450,7 +456,7 @@ describe('Type cachedNamespace Should be enabled', () => {
describe('ExtractionNamespace type JDBC', () => { describe('ExtractionNamespace type JDBC', () => {
it('No namespace', () => { it('No namespace', () => {
expect( expect(
isDisabled('lookup', 'v1', '__default', { isLookupSubmitDisabled('lookup', 'v1', '__default', {
type: 'cachedNamespace', type: 'cachedNamespace',
extractionNamespace: { extractionNamespace: {
type: 'jdbc', type: 'jdbc',

View File

@ -76,7 +76,7 @@ export interface LookupSpec {
export interface LookupEditDialogProps { export interface LookupEditDialogProps {
onClose: () => void; onClose: () => void;
onSubmit: (updateLookupVersion: boolean) => void; onSubmit: (updateLookupVersion: boolean) => void;
onChange: (field: string, value: string | LookupSpec) => void; onChange: (field: 'name' | 'tier' | 'version' | 'spec', value: string | LookupSpec) => void;
lookupName: string; lookupName: string;
lookupTier: string; lookupTier: string;
lookupVersion: string; lookupVersion: string;
@ -85,11 +85,11 @@ export interface LookupEditDialogProps {
allLookupTiers: string[]; allLookupTiers: string[];
} }
export function isDisabled( export function isLookupSubmitDisabled(
lookupName?: string, lookupName: string | undefined,
lookupVersion?: string, lookupVersion: string | undefined,
lookupTier?: string, lookupTier: string | undefined,
lookupSpec?: LookupSpec, lookupSpec: LookupSpec | undefined,
) { ) {
let disableSubmit = let disableSubmit =
!lookupName || !lookupName ||
@ -141,6 +141,405 @@ export function isDisabled(
} }
return disableSubmit; 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',
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) =>
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) { export const LookupEditDialog = React.memo(function LookupEditDialog(props: LookupEditDialogProps) {
const { const {
onClose, onClose,
@ -159,7 +558,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
function addISOVersion() { function addISOVersion() {
const currentDate = new Date(); const currentDate = new Date();
const ISOString = currentDate.toISOString(); const ISOString = currentDate.toISOString();
onChange('lookupEditVersion', ISOString); onChange('version', ISOString);
} }
function renderTierInput() { function renderTierInput() {
@ -170,7 +569,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
value={lookupTier} value={lookupTier}
onChange={(e: any) => { onChange={(e: any) => {
updateVersionOnSubmit = false; updateVersionOnSubmit = false;
onChange('lookupEditTier', e.target.value); onChange('tier', e.target.value);
}} }}
disabled disabled
/> />
@ -182,7 +581,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
<HTMLSelect <HTMLSelect
disabled={isEdit} disabled={isEdit}
value={lookupTier} value={lookupTier}
onChange={(e: any) => onChange('lookupEditTier', e.target.value)} onChange={(e: any) => onChange('tier', e.target.value)}
> >
{allLookupTiers.map(tier => ( {allLookupTiers.map(tier => (
<option key={tier} value={tier}> <option key={tier} value={tier}>
@ -195,384 +594,6 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
} }
} }
const fields = [
{
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
className="lookup-edit-dialog" className="lookup-edit-dialog"
@ -583,7 +604,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
<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('name', e.target.value)}
disabled={isEdit} disabled={isEdit}
placeholder="Enter the lookup name" placeholder="Enter the lookup name"
/> />
@ -592,7 +613,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
<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('version', e.target.value)}
placeholder="Enter the lookup version" placeholder="Enter the lookup version"
rightElement={ rightElement={
<Button minimal text="Use ISO as version" onClick={() => addISOVersion()} /> <Button minimal text="Use ISO as version" onClick={() => addISOVersion()} />
@ -600,10 +621,10 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
/> />
</FormGroup> </FormGroup>
<AutoForm <AutoForm
fields={fields as Field<LookupSpec>[]} fields={LOOKUP_FIELDS}
model={lookupSpec} model={lookupSpec}
onChange={m => { onChange={m => {
onChange('lookupEditSpec', m); onChange('spec', m);
}} }}
/> />
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
@ -615,7 +636,7 @@ export const LookupEditDialog = React.memo(function LookupEditDialog(props: Look
onClick={() => { onClick={() => {
onSubmit(updateVersionOnSubmit && isEdit); onSubmit(updateVersionOnSubmit && isEdit);
}} }}
disabled={isDisabled(lookupName, lookupVersion, lookupTier, lookupSpec)} disabled={isLookupSubmitDisabled(lookupName, lookupVersion, lookupTier, lookupSpec)}
/> />
</div> </div>
</div> </div>

View File

@ -16,9 +16,9 @@
* limitations under the License. * limitations under the License.
*/ */
// This is set to the latest available version and should be updated to the next version before release
import hasOwnProp from 'has-own-prop'; import hasOwnProp from 'has-own-prop';
// This is set to the latest available version and should be updated to the next version before release
const DRUID_DOCS_VERSION = '0.19.0'; const DRUID_DOCS_VERSION = '0.19.0';
function fillVersion(str: string): string { function fillVersion(str: string): string {

View File

@ -185,10 +185,10 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
} }
} }
private handleChangeLookup = (field: string, value: string | LookupSpec) => { private handleChangeLookup = (field: keyof LookupEditInfo, value: string | LookupSpec) => {
this.setState({ this.setState(state => ({
[field]: value, lookupEdit: Object.assign({}, state.lookupEdit, { [field]: value }),
} as any); }));
}; };
private async submitLookupEdit(updatelookupEditVersion: boolean) { private async submitLookupEdit(updatelookupEditVersion: boolean) {

View File

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ColumnRenameInput matches snapshot 1`] = `
<Blueprint3.InputGroup
autoFocus={true}
className="column-rename-input"
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
small={true}
value="hello"
/>
`;

View File

@ -0,0 +1,30 @@
/*
* 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 { shallow } from 'enzyme';
import React from 'react';
import { ColumnRenameInput } from './column-rename-input';
describe('ColumnRenameInput', () => {
it('matches snapshot', () => {
const columnRenameInput = shallow(<ColumnRenameInput initialName="hello" onDone={() => {}} />);
expect(columnRenameInput).toMatchSnapshot();
});
});

View File

@ -0,0 +1,62 @@
/*
* 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 } from '@blueprintjs/core';
import React, { useState } from 'react';
export interface ColumnRenameInputProps {
initialName: string;
onDone: (newName?: string) => void;
}
export const ColumnRenameInput = React.memo(function ColumnRenameInput(
props: ColumnRenameInputProps,
) {
const { initialName, onDone } = props;
const [newName, setNewName] = useState<string>(initialName);
function maybeDone() {
if (newName && newName !== initialName) {
onDone(newName);
} else {
onDone();
}
}
return (
<InputGroup
className="column-rename-input"
value={newName}
onChange={(e: any) => setNewName(e.target.value)}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.keyCode) {
case 13: // Enter
maybeDone();
break;
case 27: // Esc
onDone();
break;
}
}}
onBlur={maybeDone}
small
autoFocus
/>
);
});

View File

@ -24,35 +24,46 @@
width: 100%; width: 100%;
font-feature-settings: tnum; font-feature-settings: tnum;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
.rt-thead {
.rt-th {
&.aggregate-header {
background: rgb(75, 122, 148);
}
&.renaming {
padding: 0;
}
.asc {
box-shadow: inset 0 3px 0 0 rgba(255, 255, 255, 0.6);
}
.desc {
box-shadow: inset 0 -3px 0 0 rgba(255, 255, 255, 0.6);
}
.bp3-icon {
margin-left: 3px;
}
}
}
.rt-td {
cursor: pointer;
}
} }
.clickable-cell { .clickable-cell {
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
} }
.bp3-popover-target { .bp3-popover-target {
width: 100%; width: 100%;
} }
.aggregate-column { .aggregate-column {
background-color: rgba(98, 205, 255, 0.1); background-color: rgba(98, 205, 255, 0.1);
} }
.rt-th {
&.aggregate-header {
background: rgb(75, 122, 148);
}
.asc {
box-shadow: inset 0 3px 0 0 rgba(255, 255, 255, 0.6);
}
.desc {
box-shadow: inset 0 -3px 0 0 rgba(255, 255, 255, 0.6);
}
.bp3-icon {
margin-left: 3px;
}
}
.rt-td {
cursor: pointer;
}
} }

View File

@ -34,6 +34,8 @@ import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-d
import { copyAndAlert, prettyPrintSql } from '../../../utils'; import { copyAndAlert, prettyPrintSql } from '../../../utils';
import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action'; import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
import { ColumnRenameInput } from './column-rename-input/column-rename-input';
import './query-output.scss'; import './query-output.scss';
function isComparable(x: unknown): boolean { function isComparable(x: unknown): boolean {
@ -59,9 +61,10 @@ export interface QueryOutputProps {
} }
export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputProps) { export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputProps) {
const { queryResult } = props; const { queryResult, onQueryChange, runeMode } = props;
const parsedQuery = queryResult ? queryResult.sqlQuery : undefined; const parsedQuery = queryResult ? queryResult.sqlQuery : undefined;
const [showValue, setShowValue] = useState(); const [showValue, setShowValue] = useState<string>();
const [renamingColumn, setRenamingColumn] = useState<number>(-1);
function hasFilterOnHeader(header: string, headerIndex: number): boolean { function hasFilterOnHeader(header: string, headerIndex: number): boolean {
if (!parsedQuery || !parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) return false; if (!parsedQuery || !parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) return false;
@ -73,7 +76,6 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
} }
function getHeaderMenu(header: string, headerIndex: number) { function getHeaderMenu(header: string, headerIndex: number) {
const { onQueryChange, runeMode } = props;
const ref = SqlRef.column(header); const ref = SqlRef.column(header);
const prettyRef = prettyPrintSql(ref); const prettyRef = prettyPrintSql(ref);
@ -145,6 +147,16 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
} }
} }
if (!parsedQuery.hasStarInSelect()) {
basicActions.push({
icon: IconNames.EDIT,
title: `Rename column`,
onAction: () => {
setRenamingColumn(headerIndex);
},
});
}
basicActions.push({ basicActions.push({
icon: IconNames.CROSS, icon: IconNames.CROSS,
title: `Remove column`, title: `Remove column`,
@ -291,7 +303,7 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
} }
} }
function getHeaderClassName(header: string) { function getHeaderClassName(header: string, i: number) {
if (!parsedQuery) return; if (!parsedQuery) return;
const className = []; const className = [];
@ -304,9 +316,31 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
className.push('aggregate-header'); className.push('aggregate-header');
} }
if (i === renamingColumn) {
className.push('renaming');
}
return className.join(' '); return className.join(' ');
} }
function renameColumnTo(renameTo: string | undefined) {
setRenamingColumn(-1);
if (renameTo && parsedQuery) {
if (parsedQuery.hasStarInSelect()) return;
const selectExpression = parsedQuery.selectExpressions.get(renamingColumn);
if (!selectExpression) return;
onQueryChange(
parsedQuery.changeSelectExpressions(
parsedQuery.selectExpressions.change(
renamingColumn,
selectExpression.changeAliasName(renameTo),
),
),
true,
);
}
}
return ( return (
<div className="query-output"> <div className="query-output">
<ReactTable <ReactTable
@ -316,17 +350,22 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
columns={(queryResult ? queryResult.header : []).map((column, i) => { columns={(queryResult ? queryResult.header : []).map((column, i) => {
const h = column.name; const h = column.name;
return { return {
Header: () => { Header:
return ( i === renamingColumn && parsedQuery
<Popover className={'clickable-cell'} content={getHeaderMenu(h, i)}> ? () => <ColumnRenameInput initialName={h} onDone={renameColumnTo} />
<div> : () => {
{h} return (
{hasFilterOnHeader(h, i) && <Icon icon={IconNames.FILTER} iconSize={14} />} <Popover className={'clickable-cell'} content={getHeaderMenu(h, i)}>
</div> <div>
</Popover> {h}
); {hasFilterOnHeader(h, i) && (
}, <Icon icon={IconNames.FILTER} iconSize={14} />
headerClassName: getHeaderClassName(h), )}
</div>
</Popover>
);
},
headerClassName: getHeaderClassName(h, i),
accessor: String(i), accessor: String(i),
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;