mirror of
https://github.com/apache/druid.git
synced 2025-02-17 07:25:02 +00:00
Web console: streaming json input format specifics (#13381)
* streaming json input format specifics * goodies
This commit is contained in:
parent
a3d45f6086
commit
c628947c31
@ -19,7 +19,7 @@ exports[`ShowJson matches snapshot 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bp4-button-text"
|
class="bp4-button-text"
|
||||||
>
|
>
|
||||||
Save
|
Download
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
@ -56,7 +56,7 @@ export const ShowJson = React.memo(function ShowJson(props: ShowJsonProps) {
|
|||||||
{downloadFilename && (
|
{downloadFilename && (
|
||||||
<Button
|
<Button
|
||||||
disabled={jsonState.loading}
|
disabled={jsonState.loading}
|
||||||
text="Save"
|
text="Download"
|
||||||
minimal
|
minimal
|
||||||
onClick={() => downloadFile(jsonValue, 'json', downloadFilename)}
|
onClick={() => downloadFile(jsonValue, 'json', downloadFilename)}
|
||||||
/>
|
/>
|
||||||
|
@ -32,7 +32,7 @@ exports[`ShowLog describe show log 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bp4-button-text"
|
class="bp4-button-text"
|
||||||
>
|
>
|
||||||
Save
|
Download
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
|
@ -155,7 +155,7 @@ export class ShowLog extends React.PureComponent<ShowLogProps, ShowLogState> {
|
|||||||
<ButtonGroup className="right-buttons">
|
<ButtonGroup className="right-buttons">
|
||||||
{downloadFilename && (
|
{downloadFilename && (
|
||||||
<AnchorButton
|
<AnchorButton
|
||||||
text="Save"
|
text="Download"
|
||||||
minimal
|
minimal
|
||||||
download={downloadFilename}
|
download={downloadFilename}
|
||||||
href={UrlBaser.base(endpoint)}
|
href={UrlBaser.base(endpoint)}
|
||||||
|
@ -17,7 +17,7 @@ exports[`ShowValue matches snapshot 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bp4-button-text"
|
class="bp4-button-text"
|
||||||
>
|
>
|
||||||
Save
|
Download
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,7 +41,7 @@ export const ShowValue = React.memo(function ShowValue(props: ShowValueProps) {
|
|||||||
)}
|
)}
|
||||||
{downloadFilename && (
|
{downloadFilename && (
|
||||||
<Button
|
<Button
|
||||||
text="Save"
|
text="Download"
|
||||||
minimal
|
minimal
|
||||||
onClick={() => downloadFile(jsonValue, 'json', downloadFilename)}
|
onClick={() => downloadFile(jsonValue, 'json', downloadFilename)}
|
||||||
/>
|
/>
|
||||||
|
@ -138,7 +138,7 @@ exports[`SegmentTableActionDialog matches snapshot 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bp4-button-text"
|
class="bp4-button-text"
|
||||||
>
|
>
|
||||||
Save
|
Download
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
@ -192,7 +192,7 @@ exports[`SupervisorTableActionDialog matches snapshot 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bp4-button-text"
|
class="bp4-button-text"
|
||||||
>
|
>
|
||||||
Save
|
Download
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
@ -192,7 +192,7 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bp4-button-text"
|
class="bp4-button-text"
|
||||||
>
|
>
|
||||||
Save
|
Download
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
@ -285,12 +285,8 @@ export function getSpecType(spec: Partial<IngestionSpec>): IngestionType {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTask(spec: Partial<IngestionSpec>) {
|
export function isStreamingSpec(spec: Partial<IngestionSpec>): boolean {
|
||||||
const type = String(getSpecType(spec));
|
return oneOf(getSpecType(spec), 'kafka', 'kinesis');
|
||||||
return (
|
|
||||||
type.startsWith('index_') ||
|
|
||||||
oneOf(type, 'index', 'compact', 'kill', 'append', 'merge', 'same_interval_merge')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDruidSource(spec: Partial<IngestionSpec>): boolean {
|
export function isDruidSource(spec: Partial<IngestionSpec>): boolean {
|
||||||
|
@ -21,7 +21,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { AutoForm, ExternalLink, Field } from '../../components';
|
import { AutoForm, ExternalLink, Field } from '../../components';
|
||||||
import { getLink } from '../../links';
|
import { getLink } from '../../links';
|
||||||
import { oneOf, typeIs } from '../../utils';
|
import { compact, oneOf, typeIs } from '../../utils';
|
||||||
import { FlattenSpec } from '../flatten-spec/flatten-spec';
|
import { FlattenSpec } from '../flatten-spec/flatten-spec';
|
||||||
|
|
||||||
export interface InputFormat {
|
export interface InputFormat {
|
||||||
@ -36,136 +36,200 @@ export interface InputFormat {
|
|||||||
readonly flattenSpec?: FlattenSpec | null;
|
readonly flattenSpec?: FlattenSpec | null;
|
||||||
readonly featureSpec?: Record<string, boolean>;
|
readonly featureSpec?: Record<string, boolean>;
|
||||||
readonly keepNullColumns?: boolean;
|
readonly keepNullColumns?: boolean;
|
||||||
|
readonly assumeNewlineDelimited?: boolean;
|
||||||
|
readonly useJsonNodeReader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INPUT_FORMAT_FIELDS: Field<InputFormat>[] = [
|
function generateInputFormatFields(streaming: boolean) {
|
||||||
{
|
return compact([
|
||||||
name: 'type',
|
{
|
||||||
label: 'Input format',
|
name: 'type',
|
||||||
type: 'string',
|
label: 'Input format',
|
||||||
suggestions: ['json', 'csv', 'tsv', 'parquet', 'orc', 'avro_ocf', 'avro_stream', 'regex'],
|
type: 'string',
|
||||||
required: true,
|
suggestions: ['json', 'csv', 'tsv', 'parquet', 'orc', 'avro_ocf', 'avro_stream', 'regex'],
|
||||||
info: (
|
required: true,
|
||||||
<>
|
info: (
|
||||||
<p>The parser used to parse the data.</p>
|
<>
|
||||||
<p>
|
<p>The parser used to parse the data.</p>
|
||||||
For more information see{' '}
|
<p>
|
||||||
<ExternalLink href={`${getLink('DOCS')}/ingestion/data-formats.html`}>
|
For more information see{' '}
|
||||||
the documentation
|
<ExternalLink href={`${getLink('DOCS')}/ingestion/data-formats.html`}>
|
||||||
</ExternalLink>
|
the documentation
|
||||||
.
|
</ExternalLink>
|
||||||
</p>
|
.
|
||||||
</>
|
</p>
|
||||||
),
|
</>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
name: 'featureSpec',
|
{
|
||||||
label: 'JSON parser features',
|
name: 'featureSpec',
|
||||||
type: 'json',
|
label: 'JSON parser features',
|
||||||
defined: typeIs('json'),
|
type: 'json',
|
||||||
info: (
|
defined: typeIs('json'),
|
||||||
<>
|
info: (
|
||||||
<p>
|
<>
|
||||||
<ExternalLink href="https://github.com/FasterXML/jackson-core/wiki/JsonParser-Features">
|
<p>
|
||||||
JSON parser features
|
<ExternalLink href="https://github.com/FasterXML/jackson-core/wiki/JsonParser-Features">
|
||||||
</ExternalLink>{' '}
|
JSON parser features
|
||||||
supported by Jackson library. Those features will be applied when parsing the input JSON
|
</ExternalLink>{' '}
|
||||||
data.
|
supported by Jackson library. Those features will be applied when parsing the input JSON
|
||||||
</p>
|
data.
|
||||||
<p>
|
</p>
|
||||||
Example:{' '}
|
<p>
|
||||||
<Code>{`{ "ALLOW_SINGLE_QUOTES": true, "ALLOW_UNQUOTED_FIELD_NAMES": true }`}</Code>
|
Example:{' '}
|
||||||
</p>
|
<Code>{`{ "ALLOW_SINGLE_QUOTES": true, "ALLOW_UNQUOTED_FIELD_NAMES": true }`}</Code>
|
||||||
</>
|
</p>
|
||||||
),
|
</>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
name: 'delimiter',
|
streaming
|
||||||
type: 'string',
|
? {
|
||||||
defaultValue: '\t',
|
name: 'assumeNewlineDelimited',
|
||||||
suggestions: ['\t', ';', '|', '#'],
|
type: 'boolean',
|
||||||
defined: typeIs('tsv'),
|
defined: typeIs('json'),
|
||||||
info: <>A custom delimiter for data values.</>,
|
disabled: (inputFormat: InputFormat) => inputFormat.useJsonNodeReader,
|
||||||
},
|
defaultValue: false,
|
||||||
{
|
info: (
|
||||||
name: 'pattern',
|
<>
|
||||||
type: 'string',
|
<p>
|
||||||
defined: typeIs('regex'),
|
In streaming ingestion, multi-line JSON events can be ingested (i.e. where a single
|
||||||
required: true,
|
JSON event spans multiple lines). However, if a parsing exception occurs, all JSON
|
||||||
},
|
events that are present in the same streaming record will be discarded.
|
||||||
{
|
</p>
|
||||||
name: 'function',
|
<p>
|
||||||
type: 'string',
|
<Code>assumeNewlineDelimited</Code> and <Code>useJsonNodeReader</Code> (at most one
|
||||||
defined: typeIs('javascript'),
|
can be <Code>true</Code>) affect only how parsing exceptions are handled.
|
||||||
required: true,
|
</p>
|
||||||
},
|
<p>
|
||||||
{
|
If the input is known to be newline delimited JSON (each individual JSON event is
|
||||||
name: 'skipHeaderRows',
|
contained in a single line, separated by newlines), setting this option to true
|
||||||
type: 'number',
|
allows for more flexible parsing exception handling. Only the lines with invalid
|
||||||
defaultValue: 0,
|
JSON syntax will be discarded, while lines containing valid JSON events will still
|
||||||
defined: typeIs('csv', 'tsv'),
|
be ingested.
|
||||||
min: 0,
|
</p>
|
||||||
info: (
|
</>
|
||||||
<>
|
),
|
||||||
If this is set, skip the first <Code>skipHeaderRows</Code> rows from each file.
|
}
|
||||||
</>
|
: undefined,
|
||||||
),
|
streaming
|
||||||
},
|
? {
|
||||||
{
|
name: 'useJsonNodeReader',
|
||||||
name: 'findColumnsFromHeader',
|
type: 'boolean',
|
||||||
type: 'boolean',
|
defined: typeIs('json'),
|
||||||
defined: typeIs('csv', 'tsv'),
|
disabled: (inputFormat: InputFormat) => inputFormat.assumeNewlineDelimited,
|
||||||
required: true,
|
defaultValue: false,
|
||||||
info: (
|
info: (
|
||||||
<>
|
<>
|
||||||
If this is set, find the column names from the header row. Note that
|
{' '}
|
||||||
<Code>skipHeaderRows</Code> will be applied before finding column names from the header. For
|
<p>
|
||||||
example, if you set <Code>skipHeaderRows</Code> to 2 and <Code>findColumnsFromHeader</Code>{' '}
|
In streaming ingestion, multi-line JSON events can be ingested (i.e. where a single
|
||||||
to true, the task will skip the first two lines and then extract column information from the
|
JSON event spans multiple lines). However, if a parsing exception occurs, all JSON
|
||||||
third line.
|
events that are present in the same streaming record will be discarded.
|
||||||
</>
|
</p>
|
||||||
),
|
<p>
|
||||||
},
|
<Code>assumeNewlineDelimited</Code> and <Code>useJsonNodeReader</Code> (at most one
|
||||||
{
|
can be <Code>true</Code>) affect only how parsing exceptions are handled.
|
||||||
name: 'columns',
|
</p>
|
||||||
type: 'string-array',
|
<p>
|
||||||
required: true,
|
When ingesting multi-line JSON events, enabling this option will enable the use of a
|
||||||
defined: p =>
|
JSON parser which will retain any valid JSON events encountered within a streaming
|
||||||
(oneOf(p.type, 'csv', 'tsv') && p.findColumnsFromHeader === false) || p.type === 'regex',
|
record prior to when a parsing exception occurred.
|
||||||
info: (
|
</p>
|
||||||
<>
|
</>
|
||||||
Specifies the columns of the data. The columns should be in the same order with the columns
|
),
|
||||||
of your data.
|
}
|
||||||
</>
|
: undefined,
|
||||||
),
|
{
|
||||||
},
|
name: 'delimiter',
|
||||||
{
|
type: 'string',
|
||||||
name: 'listDelimiter',
|
defaultValue: '\t',
|
||||||
type: 'string',
|
suggestions: ['\t', ';', '|', '#'],
|
||||||
defaultValue: '\x01',
|
defined: typeIs('tsv'),
|
||||||
suggestions: ['\x01', '\x00'],
|
info: <>A custom delimiter for data values.</>,
|
||||||
defined: typeIs('csv', 'tsv', 'regex'),
|
},
|
||||||
info: <>A custom delimiter for multi-value dimensions.</>,
|
{
|
||||||
},
|
name: 'pattern',
|
||||||
{
|
type: 'string',
|
||||||
name: 'binaryAsString',
|
defined: typeIs('regex'),
|
||||||
type: 'boolean',
|
required: true,
|
||||||
defaultValue: false,
|
},
|
||||||
defined: typeIs('parquet', 'orc', 'avro_ocf', 'avro_stream'),
|
{
|
||||||
info: (
|
name: 'function',
|
||||||
<>
|
type: 'string',
|
||||||
Specifies if the binary column which is not logically marked as a string should be treated
|
defined: typeIs('javascript'),
|
||||||
as a UTF-8 encoded string.
|
required: true,
|
||||||
</>
|
},
|
||||||
),
|
{
|
||||||
},
|
name: 'skipHeaderRows',
|
||||||
];
|
type: 'number',
|
||||||
|
defaultValue: 0,
|
||||||
|
defined: typeIs('csv', 'tsv'),
|
||||||
|
min: 0,
|
||||||
|
info: (
|
||||||
|
<>
|
||||||
|
If this is set, skip the first <Code>skipHeaderRows</Code> rows from each file.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'findColumnsFromHeader',
|
||||||
|
type: 'boolean',
|
||||||
|
defined: typeIs('csv', 'tsv'),
|
||||||
|
required: true,
|
||||||
|
info: (
|
||||||
|
<>
|
||||||
|
If this is set, find the column names from the header row. Note that
|
||||||
|
<Code>skipHeaderRows</Code> will be applied before finding column names from the header.
|
||||||
|
For example, if you set <Code>skipHeaderRows</Code> to 2 and{' '}
|
||||||
|
<Code>findColumnsFromHeader</Code> to true, the task will skip the first two lines and
|
||||||
|
then extract column information from the third line.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'columns',
|
||||||
|
type: 'string-array',
|
||||||
|
required: true,
|
||||||
|
defined: p =>
|
||||||
|
(oneOf(p.type, 'csv', 'tsv') && p.findColumnsFromHeader === false) || p.type === 'regex',
|
||||||
|
info: (
|
||||||
|
<>
|
||||||
|
Specifies the columns of the data. The columns should be in the same order with the
|
||||||
|
columns of your data.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'listDelimiter',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: '\x01',
|
||||||
|
suggestions: ['\x01', '\x00'],
|
||||||
|
defined: typeIs('csv', 'tsv', 'regex'),
|
||||||
|
info: <>A custom delimiter for multi-value dimensions.</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'binaryAsString',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
defined: typeIs('parquet', 'orc', 'avro_ocf', 'avro_stream'),
|
||||||
|
info: (
|
||||||
|
<>
|
||||||
|
Specifies if the binary column which is not logically marked as a string should be treated
|
||||||
|
as a UTF-8 encoded string.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
] as (Field<InputFormat> | undefined)[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const INPUT_FORMAT_FIELDS: Field<InputFormat>[] = generateInputFormatFields(false);
|
||||||
|
export const STREAMING_INPUT_FORMAT_FIELDS: Field<InputFormat>[] = generateInputFormatFields(true);
|
||||||
|
|
||||||
export function issueWithInputFormat(inputFormat: InputFormat | undefined): string | undefined {
|
export function issueWithInputFormat(inputFormat: InputFormat | undefined): string | undefined {
|
||||||
return AutoForm.issueWithModel(inputFormat, INPUT_FORMAT_FIELDS);
|
return AutoForm.issueWithModel(inputFormat, INPUT_FORMAT_FIELDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const inputFormatCanFlatten: (inputFormat: InputFormat) => boolean = typeIs(
|
export const inputFormatCanProduceNestedData: (inputFormat: InputFormat) => boolean = typeIs(
|
||||||
'json',
|
'json',
|
||||||
'parquet',
|
'parquet',
|
||||||
'orc',
|
'orc',
|
||||||
|
@ -57,30 +57,36 @@ export const ConnectMessage = React.memo(function ConnectMessage(props: ConnectM
|
|||||||
});
|
});
|
||||||
|
|
||||||
export interface ParserMessageProps {
|
export interface ParserMessageProps {
|
||||||
canFlatten: boolean;
|
canHaveNestedData: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ParserMessage = React.memo(function ParserMessage(props: ParserMessageProps) {
|
export const ParserMessage = React.memo(function ParserMessage(props: ParserMessageProps) {
|
||||||
const { canFlatten } = props;
|
const { canHaveNestedData } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<Callout>
|
<Callout>
|
||||||
<p>
|
<p>
|
||||||
You can{' '}
|
Druid needs to parse data as columns. Determine the format of your data and ensure that
|
||||||
<ExternalLink href={`${getLink('DOCS')}/querying/nested-columns.html`}>
|
the columns are accurately parsed.
|
||||||
directly ingest nested data
|
|
||||||
</ExternalLink>{' '}
|
|
||||||
into COMPLEX<json> columns.
|
|
||||||
</p>
|
</p>
|
||||||
{canFlatten && (
|
{canHaveNestedData && (
|
||||||
<p>
|
<>
|
||||||
If you have nested data, you can{' '}
|
<p>
|
||||||
<ExternalLink href={`${getLink('DOCS')}/ingestion/index.html#flattenspec`}>
|
If you have nested data, you can ingest it into{' '}
|
||||||
flatten
|
<ExternalLink href={`${getLink('DOCS')}/querying/nested-columns.html`}>
|
||||||
</ExternalLink>{' '}
|
COMPLEX<json>
|
||||||
it here.
|
</ExternalLink>{' '}
|
||||||
</p>
|
columns.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Alternatively, you can explicitly{' '}
|
||||||
|
<ExternalLink href={`${getLink('DOCS')}/ingestion/index.html#flattenspec`}>
|
||||||
|
flatten
|
||||||
|
</ExternalLink>{' '}
|
||||||
|
it here.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<LearnMore href={`${getLink('DOCS')}/ingestion/data-formats.html`} />
|
<LearnMore href={`${getLink('DOCS')}/ingestion/data-formats.html`} />
|
||||||
</Callout>
|
</Callout>
|
||||||
|
@ -85,7 +85,6 @@ import {
|
|||||||
getRequiredModule,
|
getRequiredModule,
|
||||||
getRollup,
|
getRollup,
|
||||||
getSecondaryPartitionRelatedFormFields,
|
getSecondaryPartitionRelatedFormFields,
|
||||||
getSpecType,
|
|
||||||
getTimestampExpressionFields,
|
getTimestampExpressionFields,
|
||||||
getTimestampSchema,
|
getTimestampSchema,
|
||||||
getTuningFormFields,
|
getTuningFormFields,
|
||||||
@ -93,15 +92,15 @@ import {
|
|||||||
IngestionSpec,
|
IngestionSpec,
|
||||||
INPUT_FORMAT_FIELDS,
|
INPUT_FORMAT_FIELDS,
|
||||||
InputFormat,
|
InputFormat,
|
||||||
inputFormatCanFlatten,
|
inputFormatCanProduceNestedData,
|
||||||
invalidIoConfig,
|
invalidIoConfig,
|
||||||
invalidPartitionConfig,
|
invalidPartitionConfig,
|
||||||
IoConfig,
|
IoConfig,
|
||||||
isDruidSource,
|
isDruidSource,
|
||||||
isEmptyIngestionSpec,
|
isEmptyIngestionSpec,
|
||||||
|
isStreamingSpec,
|
||||||
issueWithIoConfig,
|
issueWithIoConfig,
|
||||||
issueWithSampleData,
|
issueWithSampleData,
|
||||||
isTask,
|
|
||||||
joinFilter,
|
joinFilter,
|
||||||
KNOWN_FILTER_TYPES,
|
KNOWN_FILTER_TYPES,
|
||||||
MAX_INLINE_DATA_LENGTH,
|
MAX_INLINE_DATA_LENGTH,
|
||||||
@ -113,6 +112,7 @@ import {
|
|||||||
PRIMARY_PARTITION_RELATED_FORM_FIELDS,
|
PRIMARY_PARTITION_RELATED_FORM_FIELDS,
|
||||||
removeTimestampTransform,
|
removeTimestampTransform,
|
||||||
splitFilter,
|
splitFilter,
|
||||||
|
STREAMING_INPUT_FORMAT_FIELDS,
|
||||||
TIME_COLUMN,
|
TIME_COLUMN,
|
||||||
TIMESTAMP_SPEC_FIELDS,
|
TIMESTAMP_SPEC_FIELDS,
|
||||||
TimestampSpec,
|
TimestampSpec,
|
||||||
@ -140,7 +140,6 @@ import {
|
|||||||
localStorageSetJson,
|
localStorageSetJson,
|
||||||
moveElement,
|
moveElement,
|
||||||
moveToIndex,
|
moveToIndex,
|
||||||
oneOf,
|
|
||||||
pluralIfNeeded,
|
pluralIfNeeded,
|
||||||
QueryState,
|
QueryState,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
@ -1205,7 +1204,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
renderConnectStep() {
|
renderConnectStep() {
|
||||||
const { inputQueryState, sampleStrategy } = this.state;
|
const { inputQueryState, sampleStrategy } = this.state;
|
||||||
const spec = this.getEffectiveSpec();
|
const spec = this.getEffectiveSpec();
|
||||||
const specType = getSpecType(spec);
|
|
||||||
const ioConfig: IoConfig = deepGet(spec, 'spec.ioConfig') || EMPTY_OBJECT;
|
const ioConfig: IoConfig = deepGet(spec, 'spec.ioConfig') || EMPTY_OBJECT;
|
||||||
const inlineMode = deepGet(spec, 'spec.ioConfig.inputSource.type') === 'inline';
|
const inlineMode = deepGet(spec, 'spec.ioConfig.inputSource.type') === 'inline';
|
||||||
const druidSource = isDruidSource(spec);
|
const druidSource = isDruidSource(spec);
|
||||||
@ -1294,7 +1292,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
</Callout>
|
</Callout>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
{oneOf(specType, 'kafka', 'kinesis') && (
|
{isStreamingSpec(spec) && (
|
||||||
<FormGroup label="Where should the data be sampled from?">
|
<FormGroup label="Where should the data be sampled from?">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
selectedValue={sampleStrategy}
|
selectedValue={sampleStrategy}
|
||||||
@ -1441,7 +1439,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
const flattenFields: FlattenField[] =
|
const flattenFields: FlattenField[] =
|
||||||
deepGet(spec, 'spec.ioConfig.inputFormat.flattenSpec.fields') || EMPTY_ARRAY;
|
deepGet(spec, 'spec.ioConfig.inputFormat.flattenSpec.fields') || EMPTY_ARRAY;
|
||||||
|
|
||||||
const canFlatten = inputFormatCanFlatten(inputFormat);
|
const canHaveNestedData = inputFormatCanProduceNestedData(inputFormat);
|
||||||
|
|
||||||
let mainFill: JSX.Element | string;
|
let mainFill: JSX.Element | string;
|
||||||
if (parserQueryState.isInit()) {
|
if (parserQueryState.isInit()) {
|
||||||
@ -1460,7 +1458,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
onChange={columnFilter => this.setState({ columnFilter })}
|
onChange={columnFilter => this.setState({ columnFilter })}
|
||||||
placeholder="Search columns"
|
placeholder="Search columns"
|
||||||
/>
|
/>
|
||||||
{canFlatten && (
|
{canHaveNestedData && (
|
||||||
<Switch
|
<Switch
|
||||||
checked={specialColumnsOnly}
|
checked={specialColumnsOnly}
|
||||||
label="Flattened columns only"
|
label="Flattened columns only"
|
||||||
@ -1473,7 +1471,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
<ParseDataTable
|
<ParseDataTable
|
||||||
sampleData={data}
|
sampleData={data}
|
||||||
columnFilter={columnFilter}
|
columnFilter={columnFilter}
|
||||||
canFlatten={canFlatten}
|
canFlatten={canHaveNestedData}
|
||||||
flattenedColumnsOnly={specialColumnsOnly}
|
flattenedColumnsOnly={specialColumnsOnly}
|
||||||
flattenFields={flattenFields}
|
flattenFields={flattenFields}
|
||||||
onFlattenFieldSelect={this.onFlattenFieldSelect}
|
onFlattenFieldSelect={this.onFlattenFieldSelect}
|
||||||
@ -1488,22 +1486,25 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
}
|
}
|
||||||
|
|
||||||
let suggestedFlattenFields: FlattenField[] | undefined;
|
let suggestedFlattenFields: FlattenField[] | undefined;
|
||||||
if (canFlatten && !flattenFields.length && parserQueryState.data) {
|
if (canHaveNestedData && !flattenFields.length && parserQueryState.data) {
|
||||||
suggestedFlattenFields = computeFlattenPathsForData(
|
suggestedFlattenFields = computeFlattenPathsForData(
|
||||||
filterMap(parserQueryState.data.rows, r => r.input),
|
filterMap(parserQueryState.data.rows, r => r.input),
|
||||||
'ignore-arrays',
|
'ignore-arrays',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputFormatFields = isStreamingSpec(spec)
|
||||||
|
? STREAMING_INPUT_FORMAT_FIELDS
|
||||||
|
: INPUT_FORMAT_FIELDS;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="main">{mainFill}</div>
|
<div className="main">{mainFill}</div>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<ParserMessage canFlatten={canFlatten} />
|
<ParserMessage canHaveNestedData={canHaveNestedData} />
|
||||||
{!selectedFlattenField && (
|
{!selectedFlattenField && (
|
||||||
<>
|
<>
|
||||||
<AutoForm
|
<AutoForm
|
||||||
fields={INPUT_FORMAT_FIELDS}
|
fields={inputFormatFields}
|
||||||
model={inputFormat}
|
model={inputFormat}
|
||||||
onChange={p =>
|
onChange={p =>
|
||||||
this.updateSpecPreview(deepSet(spec, 'spec.ioConfig.inputFormat', p))
|
this.updateSpecPreview(deepSet(spec, 'spec.ioConfig.inputFormat', p))
|
||||||
@ -1511,11 +1512,11 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
/>
|
/>
|
||||||
{this.renderApplyButtonBar(
|
{this.renderApplyButtonBar(
|
||||||
parserQueryState,
|
parserQueryState,
|
||||||
AutoForm.issueWithModel(inputFormat, INPUT_FORMAT_FIELDS),
|
AutoForm.issueWithModel(inputFormat, inputFormatFields),
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canFlatten && this.renderFlattenControls()}
|
{canHaveNestedData && this.renderFlattenControls()}
|
||||||
{suggestedFlattenFields && suggestedFlattenFields.length ? (
|
{suggestedFlattenFields && suggestedFlattenFields.length ? (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<Button
|
<Button
|
||||||
@ -1563,7 +1564,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
private readonly onFlattenFieldSelect = (field: FlattenField, index: number) => {
|
private readonly onFlattenFieldSelect = (field: FlattenField, index: number) => {
|
||||||
const { spec, unsavedChange } = this.state;
|
const { spec, unsavedChange } = this.state;
|
||||||
const inputFormat: InputFormat = deepGet(spec, 'spec.ioConfig.inputFormat') || EMPTY_OBJECT;
|
const inputFormat: InputFormat = deepGet(spec, 'spec.ioConfig.inputFormat') || EMPTY_OBJECT;
|
||||||
if (unsavedChange || !inputFormatCanFlatten(inputFormat)) return;
|
if (unsavedChange || !inputFormatCanProduceNestedData(inputFormat)) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedFlattenField: { value: field, index },
|
selectedFlattenField: { value: field, index },
|
||||||
@ -3265,7 +3266,27 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
if (submitting) return;
|
if (submitting) return;
|
||||||
|
|
||||||
this.setState({ submitting: true });
|
this.setState({ submitting: true });
|
||||||
if (isTask(spec)) {
|
if (isStreamingSpec(spec)) {
|
||||||
|
try {
|
||||||
|
await Api.instance.post('/druid/indexer/v1/supervisor', spec);
|
||||||
|
} catch (e) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: `Failed to submit supervisor: ${getDruidErrorMessage(e)}`,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
this.setState({ submitting: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Supervisor submitted successfully. Going to task view...',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
goToIngestion(undefined); // Can we get the supervisor ID here?
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
let taskResp: any;
|
let taskResp: any;
|
||||||
try {
|
try {
|
||||||
taskResp = await Api.instance.post('/druid/indexer/v1/task', spec);
|
taskResp = await Api.instance.post('/druid/indexer/v1/task', spec);
|
||||||
@ -3286,26 +3307,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
goToIngestion(taskResp.data.task);
|
goToIngestion(taskResp.data.task);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await Api.instance.post('/druid/indexer/v1/supervisor', spec);
|
|
||||||
} catch (e) {
|
|
||||||
AppToaster.show({
|
|
||||||
message: `Failed to submit supervisor: ${getDruidErrorMessage(e)}`,
|
|
||||||
intent: Intent.DANGER,
|
|
||||||
});
|
|
||||||
this.setState({ submitting: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppToaster.show({
|
|
||||||
message: 'Supervisor submitted successfully. Going to task view...',
|
|
||||||
intent: Intent.SUCCESS,
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
goToIngestion(undefined); // Can we get the supervisor ID here?
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user